Skip to content

Commit

Permalink
feat: Add formatting augmenter to console logging (#14284)
Browse files Browse the repository at this point in the history
Closes #14260
  • Loading branch information
danielwinkler authored Feb 28, 2025
1 parent cac2084 commit e5d0659
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,128 @@ public void Log_TraceInformationIsLogged()
Assert.Equal(expectedJson, actualJson);
}

public class SimpleAugmenter : IGoogleCloudConsoleLogAugmenter
{
/// <summary>
/// Augment with a simple key-value pair.
/// </summary>
public void AugmentFormattedLogEntry<TState>(in LogEntry<TState> 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
{
/// <summary>
/// Augment with a complex object.
/// </summary>
public void AugmentFormattedLogEntry<TState>(in LogEntry<TState> 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
{
/// <summary>
/// Augment with scope information.
/// </summary>
public void AugmentFormattedLogEntry<TState>(in LogEntry<TState> 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
{
/// <summary>
/// Augment with a complex object.
/// </summary>
public void AugmentFormattedLogEntry<TState>(in LogEntry<TState> 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<InvalidOperationException>(() => LogSimpleLogEntry(extraEndOptions));
}

public class DepthMismatchAugmenter : IGoogleCloudConsoleLogAugmenter
{
/// <summary>
/// Augment with a complex object.
/// </summary>
public void AugmentFormattedLogEntry<TState>(in LogEntry<TState> 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<InvalidOperationException>(() => LogSimpleLogEntry(depthMismatchOptions));
}

private static GoogleCloudConsoleFormatter CreateFormatter(GoogleCloudConsoleFormatterOptions options = null)
{
options ??= new GoogleCloudConsoleFormatterOptions();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeP
MaybeWriteFormatParameters(writer, logEntry.State);
MaybeWriteScopeInformation(writer, scopeProvider);
MaybeWriteTraceInformation(writer);
MaybeWriteLogAugmentation(writer, scopeProvider, logEntry);
writer.WriteEndObject();
writer.Flush();
}
Expand Down Expand Up @@ -209,6 +210,21 @@ private void MaybeWriteTraceInformation(Utf8JsonWriter writer)
writer.WriteBoolean(s_traceSampledPropertyName, activity.Recorded);
}

private void MaybeWriteLogAugmentation<TState>(Utf8JsonWriter writer, IExternalScopeProvider scopeProvider, in LogEntry<TState> 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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </remarks>
public string TraceGoogleCloudProjectId { get; set; }

/// <summary>
/// Allows augmenting formatted log entries with information not included by
/// <see cref="GoogleCloudConsoleFormatter"/>. May be null.
/// If set, the <see cref="IGoogleCloudConsoleLogAugmenter.AugmentFormattedLogEntry" /> will be called for each formatted entry.
/// </summary>
public IGoogleCloudConsoleLogAugmenter LogAugmenter { get; set; }
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Allows augmenting formatted log entries with information not included by <see cref="GoogleCloudConsoleFormatter"/>.
/// </summary>
public interface IGoogleCloudConsoleLogAugmenter
{
/// <summary>
/// Augments the formatted log entry with information not included by <see cref="GoogleCloudConsoleFormatter"/>.
/// </summary>
/// <typeparam name="TState">The type of the state information attached to the log entry.</typeparam>
/// <param name="logEntry">The log entry that's being formatted.</param>
/// <param name="scopeProvider">The provider of scope data.</param>
/// <param name="writer">
/// The JSON writer containing the start of the formatted log entry, meaning that
/// <see cref="Utf8JsonWriter.WriteStartObject()" /> has been called for the top level JSON object and some fields have been written
/// but <see cref="Utf8JsonWriter.WriteEndObject" /> is yet to be called for the top level JSON object.
/// Do not call <see cref="Utf8JsonWriter.WriteEndObject" /> for the top level JSON object.
/// </param>
void AugmentFormattedLogEntry<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider scopeProvider, Utf8JsonWriter writer);
}

0 comments on commit e5d0659

Please sign in to comment.