From 1fadaf164cd1d27f1e4374f89ea2131642548b19 Mon Sep 17 00:00:00 2001 From: Stanislav Perekrestov Date: Mon, 3 Jun 2024 11:45:48 +0200 Subject: [PATCH 1/6] fixes incorrect variable reference --- src/Serilog.Sinks.GoogleCloudLogging/LogFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Serilog.Sinks.GoogleCloudLogging/LogFormatter.cs b/src/Serilog.Sinks.GoogleCloudLogging/LogFormatter.cs index 191f09c..c903f73 100644 --- a/src/Serilog.Sinks.GoogleCloudLogging/LogFormatter.cs +++ b/src/Serilog.Sinks.GoogleCloudLogging/LogFormatter.cs @@ -116,7 +116,7 @@ public static string CreateLogName(string projectId, string name) // limited to 512 characters and must be url-encoded (using 500 char limit here to be safe) var safeChars = LogNameUnsafeChars.Replace(name, ""); var truncated = safeChars.Length > 500 ? safeChars.Substring(0, 500) : safeChars; - var encoded = UrlEncoder.Default.Encode(safeChars); + var encoded = UrlEncoder.Default.Encode(truncated); // LogName class creates templated string matching GCP requirements logName = new LogName(projectId, encoded).ToString(); From b5735b803a7310134b28adbd43acd3061e29a15b Mon Sep 17 00:00:00 2001 From: Stanislav Perekrestov Date: Mon, 3 Jun 2024 11:47:34 +0200 Subject: [PATCH 2/6] Adds .NET 8.0 support and upgrades to Serilog 4.0 (#71) --- .../GoogleCloudLoggingSink.cs | 4 ++-- .../GoogleCloudLoggingSinkExtensions.cs | 8 +++----- .../LogFormatter.cs | 13 ++++++++++--- .../Serilog.Sinks.GoogleCloudLogging.csproj | 12 ++++++------ src/TestWeb/TestWeb.csproj | 8 ++++---- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSink.cs b/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSink.cs index e7ab9ef..47166b0 100644 --- a/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSink.cs +++ b/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSink.cs @@ -8,9 +8,9 @@ using Google.Cloud.Logging.Type; using Google.Cloud.Logging.V2; using Google.Protobuf.WellKnownTypes; +using Serilog.Core; using Serilog.Events; using Serilog.Formatting; -using Serilog.Sinks.PeriodicBatching; namespace Serilog.Sinks.GoogleCloudLogging; @@ -62,7 +62,7 @@ public GoogleCloudLoggingSink(GoogleCloudLoggingSinkOptions sinkOptions, ITextFo : new LoggingServiceV2ClientBuilder { JsonCredentials = _sinkOptions.GoogleCredentialJson }.Build(); } - public Task EmitBatchAsync(IEnumerable events) + public Task EmitBatchAsync(IReadOnlyCollection events) { using var writer = new StringWriter(); var entries = new List(); diff --git a/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSinkExtensions.cs b/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSinkExtensions.cs index bc3c06d..943f768 100644 --- a/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSinkExtensions.cs +++ b/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSinkExtensions.cs @@ -5,7 +5,6 @@ using Serilog.Events; using Serilog.Formatting; using Serilog.Formatting.Display; -using Serilog.Sinks.PeriodicBatching; namespace Serilog.Sinks.GoogleCloudLogging; @@ -39,17 +38,16 @@ public static LoggerConfiguration GoogleCloudLogging( // formatter can be null if neither parameters are provided textFormatter ??= !String.IsNullOrWhiteSpace(outputTemplate) ? new MessageTemplateTextFormatter(outputTemplate) : null; - var batchingOptions = new PeriodicBatchingSinkOptions + var batchingOptions = new BatchingOptions { BatchSizeLimit = batchSizeLimit ?? 100, - Period = period ?? TimeSpan.FromSeconds(5), + BufferingTimeLimit = period ?? TimeSpan.FromSeconds(5), QueueLimit = queueLimit }; var sink = new GoogleCloudLoggingSink(sinkOptions, textFormatter); - var batchingSink = new PeriodicBatchingSink(sink, batchingOptions); - return loggerConfiguration.Sink(batchingSink, restrictedToMinimumLevel, levelSwitch); + return loggerConfiguration.Sink(sink, batchingOptions, restrictedToMinimumLevel, levelSwitch); } /// diff --git a/src/Serilog.Sinks.GoogleCloudLogging/LogFormatter.cs b/src/Serilog.Sinks.GoogleCloudLogging/LogFormatter.cs index c903f73..099020a 100644 --- a/src/Serilog.Sinks.GoogleCloudLogging/LogFormatter.cs +++ b/src/Serilog.Sinks.GoogleCloudLogging/LogFormatter.cs @@ -11,12 +11,19 @@ namespace Serilog.Sinks.GoogleCloudLogging; -internal class LogFormatter +internal partial class LogFormatter { private readonly ITextFormatter? _textFormatter; private static readonly Dictionary LogNameCache = new(StringComparer.Ordinal); - private static readonly Regex LogNameUnsafeChars = new("[^0-9A-Z._/-]+", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant); + + #if NET8_0_OR_GREATER + [GeneratedRegex("[^0-9A-Z._/-]+", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant)] + private static partial Regex LogNameUnsafeChars(); + #else + private static readonly Regex LogNameUnsafeCharsRegex = new("[^0-9A-Z._/-]+", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant); + private static Regex LogNameUnsafeChars() => LogNameUnsafeCharsRegex; + #endif public LogFormatter(ITextFormatter? textFormatter) { @@ -114,7 +121,7 @@ public static string CreateLogName(string projectId, string name) { // name must only contain: letters, numbers, underscore, hyphen, forward slash, period // limited to 512 characters and must be url-encoded (using 500 char limit here to be safe) - var safeChars = LogNameUnsafeChars.Replace(name, ""); + var safeChars = LogNameUnsafeChars().Replace(name, ""); var truncated = safeChars.Length > 500 ? safeChars.Substring(0, 500) : safeChars; var encoded = UrlEncoder.Default.Encode(truncated); diff --git a/src/Serilog.Sinks.GoogleCloudLogging/Serilog.Sinks.GoogleCloudLogging.csproj b/src/Serilog.Sinks.GoogleCloudLogging/Serilog.Sinks.GoogleCloudLogging.csproj index c2e8fcc..e952403 100644 --- a/src/Serilog.Sinks.GoogleCloudLogging/Serilog.Sinks.GoogleCloudLogging.csproj +++ b/src/Serilog.Sinks.GoogleCloudLogging/Serilog.Sinks.GoogleCloudLogging.csproj @@ -18,7 +18,7 @@ true snupkg true - net6.0;net5.0;netstandard2.1 + net8.0;net6.0;netstandard2.1 latest enable @@ -29,14 +29,14 @@ - - + + all runtime; build; native; contentfiles; analyzers - - - + + + diff --git a/src/TestWeb/TestWeb.csproj b/src/TestWeb/TestWeb.csproj index f6bee1d..c178191 100644 --- a/src/TestWeb/TestWeb.csproj +++ b/src/TestWeb/TestWeb.csproj @@ -1,15 +1,15 @@  - net6.0 + net8.0 default false - - - + + + From a6608ab18814e225cc2b6e268cdf274b9a656e58 Mon Sep 17 00:00:00 2001 From: Stanislav Perekrestov Date: Thu, 14 Mar 2024 17:11:20 +0100 Subject: [PATCH 3/6] propagates current trace and span ids (#67) Serilog starting from v3.1.0 adds two new first-class properties to LogEvent: TraceId and SpanId. These are set automatically in Logger.Write() to the corresponding property values from System.Diagnostics.Activity.Current. --- .../GoogleCloudLoggingSink.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSink.cs b/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSink.cs index 47166b0..90039cd 100644 --- a/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSink.cs +++ b/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSink.cs @@ -104,6 +104,18 @@ private LogEntry CreateLogEntry(LogEvent evnt, StringWriter writer) HandleSpecialProperty(log, property.Key, property.Value); } + if (_sinkOptions.UseLogCorrelation) + { + if (evnt.TraceId.ToString() is { Length: > 0 } traceId) + { + log.Trace = $"projects/{_projectId}/traces/{traceId}"; + } + if (evnt.SpanId?.ToString() is { Length: > 0 } spanId) + { + log.SpanId = spanId; + } + } + if (_serviceContext != null) jsonPayload.Fields.Add("serviceContext", Value.ForStruct(_serviceContext)); @@ -119,12 +131,6 @@ private void HandleSpecialProperty(LogEntry log, string key, LogEventPropertyVal if (_sinkOptions.UseLogCorrelation) { - if (key.Equals("TraceId", StringComparison.OrdinalIgnoreCase)) - log.Trace = $"projects/{_projectId}/traces/{GetString(value)}"; - - if (key.Equals("SpanId", StringComparison.OrdinalIgnoreCase)) - log.SpanId = GetString(value); - if (key.Equals("TraceSampled", StringComparison.OrdinalIgnoreCase)) log.TraceSampled = GetBoolean(value); } From 639d7f1ed9e1036ef324ac0cc5bf4889c76ebf44 Mon Sep 17 00:00:00 2001 From: Stanislav Perekrestov Date: Tue, 8 Jul 2025 17:02:50 -0700 Subject: [PATCH 4/6] adds .NET 9 and netstandard 2.0, drops netstandard 2.1 --- .../Serilog.Sinks.GoogleCloudLogging.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Serilog.Sinks.GoogleCloudLogging/Serilog.Sinks.GoogleCloudLogging.csproj b/src/Serilog.Sinks.GoogleCloudLogging/Serilog.Sinks.GoogleCloudLogging.csproj index e952403..03078c1 100644 --- a/src/Serilog.Sinks.GoogleCloudLogging/Serilog.Sinks.GoogleCloudLogging.csproj +++ b/src/Serilog.Sinks.GoogleCloudLogging/Serilog.Sinks.GoogleCloudLogging.csproj @@ -1,7 +1,7 @@  - 5.0.0 + 6.0.0-alpha.5 Serilog sink that writes events to Google Cloud Platform (Stackdriver) Logging. Mani Gandham MIT @@ -18,7 +18,7 @@ true snupkg true - net8.0;net6.0;netstandard2.1 + net8.0;net9.0;netstandard2.0 latest enable @@ -35,8 +35,8 @@ runtime; build; native; contentfiles; analyzers - - + + From 51ca3f76c4e83073afb30bbb59982ffcaa98d639 Mon Sep 17 00:00:00 2001 From: Stanislav Perekrestov Date: Mon, 14 Jul 2025 18:06:00 -0700 Subject: [PATCH 5/6] Reuses a StringBuilder instance to minimize the memory footprint during rendering As IBatchedLogEventSink is not called concurrently, it's considered safe to reuse a StringBuilder for message rendering --- .../GoogleCloudLoggingSink.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSink.cs b/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSink.cs index 90039cd..c784be1 100644 --- a/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSink.cs +++ b/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSink.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; using Google.Api; @@ -24,6 +25,10 @@ public class GoogleCloudLoggingSink : IBatchedLogEventSink private readonly LogFormatter _logFormatter; private readonly Struct? _serviceContext; + /// + /// Because batches aren't executed concurrently, we can reuse the stringBuilder + /// + private readonly StringBuilder _stringBuilder = new StringBuilder(); public GoogleCloudLoggingSink(GoogleCloudLoggingSinkOptions sinkOptions, ITextFormatter? textFormatter) { @@ -62,10 +67,11 @@ public GoogleCloudLoggingSink(GoogleCloudLoggingSinkOptions sinkOptions, ITextFo : new LoggingServiceV2ClientBuilder { JsonCredentials = _sinkOptions.GoogleCredentialJson }.Build(); } - public Task EmitBatchAsync(IReadOnlyCollection events) + private List CreateEventsBatch(IReadOnlyCollection events) { - using var writer = new StringWriter(); - var entries = new List(); + //writer is used for message template rendering + using var writer = new StringWriter(_stringBuilder); + var entries = new List(events.Count); foreach (var evnt in events) { @@ -79,6 +85,12 @@ public Task EmitBatchAsync(IReadOnlyCollection events) Debugging.SelfLog.WriteLine("Log entry is too large for Google Cloud Logging: {0}", GetLogEntryMessage(logEntry)); } + return entries; + } + + public Task EmitBatchAsync(IReadOnlyCollection events) + { + var entries = CreateEventsBatch(events); return entries.Count > 0 ? _client.WriteLogEntriesAsync(_logName, _resource, _sinkOptions.Labels, entries, CancellationToken.None) : Task.CompletedTask; From a51bac755ce5aced81098522342cb2302a581ba8 Mon Sep 17 00:00:00 2001 From: Stanislav Perekrestov Date: Mon, 14 Jul 2025 18:45:28 -0700 Subject: [PATCH 6/6] Adds benchmarks for event batch creation --- .../LogEventEmitBenchmark.cs | 55 +++++++++++++++++++ .../Program.cs | 6 ++ ....Sinks.GoogleCloudLogging.Benchmark.csproj | 18 ++++++ src/Serilog.Sinks.GoogleCloudLogging.sln | 6 ++ .../GoogleCloudLoggingSink.cs | 12 +++- .../Serilog.Sinks.GoogleCloudLogging.csproj | 5 ++ 6 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 src/Serilog.Sinks.GoogleCloudLogging.Benchmark/LogEventEmitBenchmark.cs create mode 100644 src/Serilog.Sinks.GoogleCloudLogging.Benchmark/Program.cs create mode 100644 src/Serilog.Sinks.GoogleCloudLogging.Benchmark/Serilog.Sinks.GoogleCloudLogging.Benchmark.csproj diff --git a/src/Serilog.Sinks.GoogleCloudLogging.Benchmark/LogEventEmitBenchmark.cs b/src/Serilog.Sinks.GoogleCloudLogging.Benchmark/LogEventEmitBenchmark.cs new file mode 100644 index 0000000..0c29a96 --- /dev/null +++ b/src/Serilog.Sinks.GoogleCloudLogging.Benchmark/LogEventEmitBenchmark.cs @@ -0,0 +1,55 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using Serilog.Events; +using Serilog.Formatting.Json; +using Serilog.Parsing; + +namespace Serilog.Sinks.GoogleCloudLogging.Benchmark; + +[MemoryDiagnoser] +[SimpleJob(runtimeMoniker: RuntimeMoniker.Net90, baseline: false)] +[SimpleJob(runtimeMoniker: RuntimeMoniker.Net80, baseline: true)] +public class LogEventEmitBenchmark +{ + private readonly GoogleCloudLoggingSink _sink = new("project_test", new JsonFormatter()); + + [Benchmark] + [ArgumentsSource(nameof(Data))] + public void CreateEventsBatch(IReadOnlyCollection events) + { + _sink.CreateEventsBatch(events); + } + + public IEnumerable> Data() + { + var timeStamp = DateTimeOffset.UtcNow; + var mtParser = new MessageTemplateParser(); + var mt = mtParser.Parse("Hello {@World}"); + return + [ + [ + new LogEvent(timestamp: timeStamp, + level: LogEventLevel.Information, + exception: null, + messageTemplate: mt, + properties: [new LogEventProperty("World", new ScalarValue("Hello World!"))]) + ], + Enumerable.Range(1, 10) + .Select(t => new LogEvent( + timestamp: timeStamp.AddMilliseconds(t), + level: LogEventLevel.Information, + exception: null, + messageTemplate: mt, + properties: [new LogEventProperty("World", new ScalarValue("Hello World!"))])) + .ToArray(), + Enumerable.Range(1, 100) + .Select(t => new LogEvent( + timestamp: timeStamp.AddMilliseconds(t), + level: LogEventLevel.Information, + exception: null, + messageTemplate: mt, + properties: [new LogEventProperty("World", new ScalarValue("Hello World!"))])) + .ToArray() + ]; + } +} diff --git a/src/Serilog.Sinks.GoogleCloudLogging.Benchmark/Program.cs b/src/Serilog.Sinks.GoogleCloudLogging.Benchmark/Program.cs new file mode 100644 index 0000000..b08c24c --- /dev/null +++ b/src/Serilog.Sinks.GoogleCloudLogging.Benchmark/Program.cs @@ -0,0 +1,6 @@ +// See https://aka.ms/new-console-template for more information + +using BenchmarkDotNet.Running; +using Serilog.Sinks.GoogleCloudLogging.Benchmark; + +_ = BenchmarkRunner.Run(); \ No newline at end of file diff --git a/src/Serilog.Sinks.GoogleCloudLogging.Benchmark/Serilog.Sinks.GoogleCloudLogging.Benchmark.csproj b/src/Serilog.Sinks.GoogleCloudLogging.Benchmark/Serilog.Sinks.GoogleCloudLogging.Benchmark.csproj new file mode 100644 index 0000000..f5f66f4 --- /dev/null +++ b/src/Serilog.Sinks.GoogleCloudLogging.Benchmark/Serilog.Sinks.GoogleCloudLogging.Benchmark.csproj @@ -0,0 +1,18 @@ + + + + Exe + net9.0;net8.0 + enable + enable + + + + + + + + + + + diff --git a/src/Serilog.Sinks.GoogleCloudLogging.sln b/src/Serilog.Sinks.GoogleCloudLogging.sln index 85e6723..b41615f 100644 --- a/src/Serilog.Sinks.GoogleCloudLogging.sln +++ b/src/Serilog.Sinks.GoogleCloudLogging.sln @@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestWeb", "TestWeb\TestWeb. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Sinks.GoogleCloudLogging.Test", "Serilog.Sinks.GoogleCloudLogging.Test\Serilog.Sinks.GoogleCloudLogging.Test.csproj", "{858BBD6D-9FF4-4D78-95A7-7139E69FCC55}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serilog.Sinks.GoogleCloudLogging.Benchmark", "Serilog.Sinks.GoogleCloudLogging.Benchmark\Serilog.Sinks.GoogleCloudLogging.Benchmark.csproj", "{FD57E195-5EDD-42B5-A722-4043636AA032}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {858BBD6D-9FF4-4D78-95A7-7139E69FCC55}.Debug|Any CPU.Build.0 = Debug|Any CPU {858BBD6D-9FF4-4D78-95A7-7139E69FCC55}.Release|Any CPU.ActiveCfg = Release|Any CPU {858BBD6D-9FF4-4D78-95A7-7139E69FCC55}.Release|Any CPU.Build.0 = Release|Any CPU + {FD57E195-5EDD-42B5-A722-4043636AA032}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD57E195-5EDD-42B5-A722-4043636AA032}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD57E195-5EDD-42B5-A722-4043636AA032}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD57E195-5EDD-42B5-A722-4043636AA032}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSink.cs b/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSink.cs index c784be1..8979024 100644 --- a/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSink.cs +++ b/src/Serilog.Sinks.GoogleCloudLogging/GoogleCloudLoggingSink.cs @@ -15,7 +15,7 @@ namespace Serilog.Sinks.GoogleCloudLogging; -public class GoogleCloudLoggingSink : IBatchedLogEventSink +public sealed class GoogleCloudLoggingSink : IBatchedLogEventSink { private readonly GoogleCloudLoggingSinkOptions _sinkOptions; private readonly LoggingServiceV2Client _client; @@ -67,7 +67,15 @@ public GoogleCloudLoggingSink(GoogleCloudLoggingSinkOptions sinkOptions, ITextFo : new LoggingServiceV2ClientBuilder { JsonCredentials = _sinkOptions.GoogleCredentialJson }.Build(); } - private List CreateEventsBatch(IReadOnlyCollection events) + //For testing and benchmarking purposes + internal GoogleCloudLoggingSink(string projectId, ITextFormatter? textFormatter) + { + _projectId = projectId; + _logFormatter = new LogFormatter(textFormatter);; + _sinkOptions = new GoogleCloudLoggingSinkOptions(projectId, useLogCorrelation: true); + } + + internal List CreateEventsBatch(IReadOnlyCollection events) { //writer is used for message template rendering using var writer = new StringWriter(_stringBuilder); diff --git a/src/Serilog.Sinks.GoogleCloudLogging/Serilog.Sinks.GoogleCloudLogging.csproj b/src/Serilog.Sinks.GoogleCloudLogging/Serilog.Sinks.GoogleCloudLogging.csproj index 03078c1..b05211a 100644 --- a/src/Serilog.Sinks.GoogleCloudLogging/Serilog.Sinks.GoogleCloudLogging.csproj +++ b/src/Serilog.Sinks.GoogleCloudLogging/Serilog.Sinks.GoogleCloudLogging.csproj @@ -38,5 +38,10 @@ + + + <_Parameter1>Serilog.Sinks.GoogleCloudLogging.Benchmark + +