diff --git a/apis/Google.Cloud.Logging.Console/Google.Cloud.Logging.Console.Tests/GoogleCloudConsoleFormatterTest.cs b/apis/Google.Cloud.Logging.Console/Google.Cloud.Logging.Console.Tests/GoogleCloudConsoleFormatterTest.cs index b974454daf39..affdc00d8a63 100644 --- a/apis/Google.Cloud.Logging.Console/Google.Cloud.Logging.Console.Tests/GoogleCloudConsoleFormatterTest.cs +++ b/apis/Google.Cloud.Logging.Console/Google.Cloud.Logging.Console.Tests/GoogleCloudConsoleFormatterTest.cs @@ -187,6 +187,128 @@ public void Log_TraceInformationIsLogged() Assert.Equal(expectedJson, actualJson); } + public class SimpleAugmenter : IGoogleCloudConsoleLogAugmenter + { + /// + /// Augment with a simple key-value pair. + /// + public void AugmentFormattedLogEntry(in LogEntry logEntry, IExternalScopeProvider scopeProvider, Utf8JsonWriter writer) + { + writer.WriteString("simple_key", "simple_value"); + } + } + + [Fact] + public void ConsoleLoggerOptions_LogAugmenter_SimplestLog() + { + var expectedAugmentedLogEntryJson = "{\"message\":\"test\",\"category\":\"LogCategory\",\"severity\":\"INFO\",\"simple_key\":\"simple_value\"}\n"; + + var simpleOptions = new GoogleCloudConsoleFormatterOptions { LogAugmenter = new SimpleAugmenter() }; + var actualJson = LogSimpleLogEntry(simpleOptions); + Assert.Equal(expectedAugmentedLogEntryJson, actualJson); + } + + public class ComplexAugmenter : IGoogleCloudConsoleLogAugmenter + { + /// + /// Augment with a complex object. + /// + public void AugmentFormattedLogEntry(in LogEntry logEntry, IExternalScopeProvider scopeProvider, Utf8JsonWriter writer) + { + writer.WriteStartObject("complex_key"); + writer.WriteString("nested_key", "nested_value"); + writer.WriteEndObject(); + } + } + + [Fact] + public void ConsoleLoggerOptions_LogAugmenter_ComplexLog() + { + var expectedAugmentedLogEntryJson = "{\"message\":\"test\",\"category\":\"LogCategory\",\"severity\":\"INFO\",\"complex_key\":{\"nested_key\":\"nested_value\"}}\n"; + + var complexOptions = new GoogleCloudConsoleFormatterOptions { LogAugmenter = new ComplexAugmenter() }; + var actualJson = LogSimpleLogEntry(complexOptions); + Assert.Equal(expectedAugmentedLogEntryJson, actualJson); + } + + public class FlatScopeAugmenter : IGoogleCloudConsoleLogAugmenter + { + /// + /// Augment with scope information. + /// + public void AugmentFormattedLogEntry(in LogEntry logEntry, IExternalScopeProvider scopeProvider, Utf8JsonWriter writer) + { + writer.WriteStartArray("augmentedScopes"); + scopeProvider.ForEachScope((scope, state) => + { + state.WriteStringValue(scope.ToString()); + }, writer); + writer.WriteEndArray(); + } + } + + [Fact] + public void ConsoleLoggerOptions_LogAugmenter_ScopeInformation() + { + var scopeProvider = new LoggerExternalScopeProvider(); + scopeProvider.Push("1 Outer Scope"); + scopeProvider.Push("2 Inner Scope"); + + var expectedAugmentedLogEntryJson = "{\"message\":\"test\",\"category\":\"LogCategory\",\"severity\":\"INFO\",\"augmentedScopes\":[\"1 Outer Scope\",\"2 Inner Scope\"]}\n"; + + var flatScopeOptions = new GoogleCloudConsoleFormatterOptions { LogAugmenter = new FlatScopeAugmenter() }; + var actualJson = LogSimpleLogEntry(flatScopeOptions, scopeProvider); + Assert.Equal(expectedAugmentedLogEntryJson, actualJson); + } + + [Fact] + public void ConsoleLoggerOptions_LogAugmenter_DefaultsToNull() + { + var defaultOptions = new GoogleCloudConsoleFormatterOptions(); + Assert.Null(defaultOptions.LogAugmenter); + } + + public class ExtraEndObjectAugmenter : IGoogleCloudConsoleLogAugmenter + { + /// + /// Augment with a complex object. + /// + public void AugmentFormattedLogEntry(in LogEntry logEntry, IExternalScopeProvider scopeProvider, Utf8JsonWriter writer) + { + writer.WriteStartObject("complex_key"); + writer.WriteString("nested_key", "nested_value"); + writer.WriteEndObject(); + writer.WriteEndObject(); // Extra WriteEndObject + } + } + + [Fact] + public void ConsoleLoggerOptions_LogAugmenter_ExtraEndObjectShouldThrow() + { + var extraEndOptions = new GoogleCloudConsoleFormatterOptions { LogAugmenter = new ExtraEndObjectAugmenter() }; + Assert.Throws(() => LogSimpleLogEntry(extraEndOptions)); + } + + public class DepthMismatchAugmenter : IGoogleCloudConsoleLogAugmenter + { + /// + /// Augment with a complex object. + /// + public void AugmentFormattedLogEntry(in LogEntry logEntry, IExternalScopeProvider scopeProvider, Utf8JsonWriter writer) + { + writer.WriteStartObject("complex_key"); + writer.WriteString("nested_key", "nested_value"); + // Missing WriteEndObject + } + } + + [Fact] + public void ConsoleLoggerOptions_LogAugmenter_DepthMismatchShouldThrow() + { + var depthMismatchOptions = new GoogleCloudConsoleFormatterOptions { LogAugmenter = new DepthMismatchAugmenter() }; + Assert.Throws(() => LogSimpleLogEntry(depthMismatchOptions)); + } + private static GoogleCloudConsoleFormatter CreateFormatter(GoogleCloudConsoleFormatterOptions options = null) { options ??= new GoogleCloudConsoleFormatterOptions(); diff --git a/apis/Google.Cloud.Logging.Console/Google.Cloud.Logging.Console/GoogleCloudConsoleFormatter.cs b/apis/Google.Cloud.Logging.Console/Google.Cloud.Logging.Console/GoogleCloudConsoleFormatter.cs index 3e05e376e54c..1acceb3f564b 100644 --- a/apis/Google.Cloud.Logging.Console/Google.Cloud.Logging.Console/GoogleCloudConsoleFormatter.cs +++ b/apis/Google.Cloud.Logging.Console/Google.Cloud.Logging.Console/GoogleCloudConsoleFormatter.cs @@ -110,6 +110,7 @@ public override void Write(in LogEntry logEntry, IExternalScopeP MaybeWriteFormatParameters(writer, logEntry.State); MaybeWriteScopeInformation(writer, scopeProvider); MaybeWriteTraceInformation(writer); + MaybeWriteLogAugmentation(writer, scopeProvider, logEntry); writer.WriteEndObject(); writer.Flush(); } @@ -209,6 +210,21 @@ private void MaybeWriteTraceInformation(Utf8JsonWriter writer) writer.WriteBoolean(s_traceSampledPropertyName, activity.Recorded); } + private void MaybeWriteLogAugmentation(Utf8JsonWriter writer, IExternalScopeProvider scopeProvider, in LogEntry logEntry) + { + if (_options.LogAugmenter is null) + { + return; + } + + var currentDepth = writer.CurrentDepth; + _options.LogAugmenter.AugmentFormattedLogEntry(logEntry, scopeProvider, writer); + if (writer.CurrentDepth != currentDepth) + { + throw new InvalidOperationException("The log augmenter must not change the depth of the JSON writer."); + } + } + private static JsonEncodedText GetSeverity(LogLevel logLevel) => logLevel switch { diff --git a/apis/Google.Cloud.Logging.Console/Google.Cloud.Logging.Console/GoogleCloudConsoleFormatterOptions.cs b/apis/Google.Cloud.Logging.Console/Google.Cloud.Logging.Console/GoogleCloudConsoleFormatterOptions.cs index b847f076a14c..c24e86ab06fd 100644 --- a/apis/Google.Cloud.Logging.Console/Google.Cloud.Logging.Console/GoogleCloudConsoleFormatterOptions.cs +++ b/apis/Google.Cloud.Logging.Console/Google.Cloud.Logging.Console/GoogleCloudConsoleFormatterOptions.cs @@ -40,4 +40,11 @@ public class GoogleCloudConsoleFormatterOptions : ConsoleFormatterOptions /// Note that when running your code in Google Cloud, for instance in Google Cloud Run, trace information is automatically collected and exported by the runtime. /// public string TraceGoogleCloudProjectId { get; set; } + + /// + /// Allows augmenting formatted log entries with information not included by + /// . May be null. + /// If set, the will be called for each formatted entry. + /// + public IGoogleCloudConsoleLogAugmenter LogAugmenter { get; set; } } diff --git a/apis/Google.Cloud.Logging.Console/Google.Cloud.Logging.Console/IGoogleCloudConsoleLogAugmenter.cs b/apis/Google.Cloud.Logging.Console/Google.Cloud.Logging.Console/IGoogleCloudConsoleLogAugmenter.cs new file mode 100644 index 000000000000..f26571a8e054 --- /dev/null +++ b/apis/Google.Cloud.Logging.Console/Google.Cloud.Logging.Console/IGoogleCloudConsoleLogAugmenter.cs @@ -0,0 +1,39 @@ +// Copyright 2025 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 Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System.Text.Json; + +namespace Google.Cloud.Logging.Console; + +/// +/// Allows augmenting formatted log entries with information not included by . +/// +public interface IGoogleCloudConsoleLogAugmenter +{ + /// + /// Augments the formatted log entry with information not included by . + /// + /// The type of the state information attached to the log entry. + /// The log entry that's being formatted. + /// The provider of scope data. + /// + /// The JSON writer containing the start of the formatted log entry, meaning that + /// has been called for the top level JSON object and some fields have been written + /// but is yet to be called for the top level JSON object. + /// Do not call for the top level JSON object. + /// + void AugmentFormattedLogEntry(in LogEntry logEntry, IExternalScopeProvider scopeProvider, Utf8JsonWriter writer); +} \ No newline at end of file