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 e7ab9ef..8979024 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; @@ -8,13 +9,13 @@ 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; -public class GoogleCloudLoggingSink : IBatchedLogEventSink +public sealed class GoogleCloudLoggingSink : IBatchedLogEventSink { private readonly GoogleCloudLoggingSinkOptions _sinkOptions; private readonly LoggingServiceV2Client _client; @@ -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,19 @@ public GoogleCloudLoggingSink(GoogleCloudLoggingSinkOptions sinkOptions, ITextFo : new LoggingServiceV2ClientBuilder { JsonCredentials = _sinkOptions.GoogleCredentialJson }.Build(); } - public Task EmitBatchAsync(IEnumerable events) + //For testing and benchmarking purposes + internal GoogleCloudLoggingSink(string projectId, ITextFormatter? textFormatter) { - using var writer = new StringWriter(); - var entries = new List(); + _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); + var entries = new List(events.Count); foreach (var evnt in events) { @@ -79,6 +93,12 @@ public Task EmitBatchAsync(IEnumerable 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; @@ -104,6 +124,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 +151,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); } 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 191f09c..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,9 +121,9 @@ 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(safeChars); + var encoded = UrlEncoder.Default.Encode(truncated); // LogName class creates templated string matching GCP requirements logName = new LogName(projectId, encoded).ToString(); diff --git a/src/Serilog.Sinks.GoogleCloudLogging/Serilog.Sinks.GoogleCloudLogging.csproj b/src/Serilog.Sinks.GoogleCloudLogging/Serilog.Sinks.GoogleCloudLogging.csproj index c2e8fcc..b05211a 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 - net6.0;net5.0;netstandard2.1 + net8.0;net9.0;netstandard2.0 latest enable @@ -29,14 +29,19 @@ - - + + all runtime; build; native; contentfiles; analyzers - - - + + + + + + + <_Parameter1>Serilog.Sinks.GoogleCloudLogging.Benchmark + 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 - - - + + +