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