diff --git a/src/Serilog.Sinks.GoogleCloudLogging.Benchmark/LogEventEmitBenchmark.cs b/src/Serilog.Sinks.GoogleCloudLogging.Benchmark/LogEventEmitBenchmark.cs new file mode 100644 index 0000000..f499235 --- /dev/null +++ b/src/Serilog.Sinks.GoogleCloudLogging.Benchmark/LogEventEmitBenchmark.cs @@ -0,0 +1,56 @@ +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.Net10_0, baseline: false)] +[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..cda5e6e --- /dev/null +++ b/src/Serilog.Sinks.GoogleCloudLogging.Benchmark/Serilog.Sinks.GoogleCloudLogging.Benchmark.csproj @@ -0,0 +1,18 @@ + + + + Exe + net9.0;net8.0;net10.0; + enable + enable + + + + + + + + + + + diff --git a/src/Serilog.Sinks.GoogleCloudLogging.Test/Serilog.Sinks.GoogleCloudLogging.Test.csproj b/src/Serilog.Sinks.GoogleCloudLogging.Test/Serilog.Sinks.GoogleCloudLogging.Test.csproj index 8c8ecca..43eaf80 100644 --- a/src/Serilog.Sinks.GoogleCloudLogging.Test/Serilog.Sinks.GoogleCloudLogging.Test.csproj +++ b/src/Serilog.Sinks.GoogleCloudLogging.Test/Serilog.Sinks.GoogleCloudLogging.Test.csproj @@ -1,20 +1,20 @@ - net6.0 + net10.0 default false - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive 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..ebf5cf8 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) { @@ -59,13 +64,25 @@ public GoogleCloudLoggingSink(GoogleCloudLoggingSinkOptions sinkOptions, ITextFo // logging client for google cloud apis _client = _sinkOptions.GoogleCredentialJson.IsNullOrWhiteSpace() ? LoggingServiceV2Client.Create() - : new LoggingServiceV2ClientBuilder { JsonCredentials = _sinkOptions.GoogleCredentialJson }.Build(); + : new LoggingServiceV2ClientBuilder { GoogleCredential = Google.Apis.Auth.OAuth2.CredentialFactory.FromJson(_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); + _client = null!; + _resource = null!; + _logName = null!; + } + + 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 +96,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,9 +127,34 @@ 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)); + // format exception for Google Cloud Error Reporting + // see: https://cloud.google.com/error-reporting/docs/formatting-error-messages + if (evnt.Exception != null) + { + //uses exception property to use the same convention as the official NLog implementation target + jsonPayload.Fields.Add("exception", Value.ForString(evnt.Exception.ToString())); + } + else if (evnt.Level >= LogEventLevel.Error) + { + //To log an error event that is a text message + jsonPayload.Fields.Add("@type", Value.ForString("type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent")); + } + log.JsonPayload = jsonPayload; return log; @@ -119,12 +167,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..1a02ae0 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; @@ -37,19 +36,18 @@ public static LoggerConfiguration GoogleCloudLogging( { // use provided text formatter or create one from output template // formatter can be null if neither parameters are provided - textFormatter ??= !String.IsNullOrWhiteSpace(outputTemplate) ? new MessageTemplateTextFormatter(outputTemplate) : null; + 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..8c87497 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) { @@ -43,7 +50,8 @@ public string RenderEventMessage(LogEvent e, StringWriter writer) if (writer.GetStringBuilder().Length > 0) writer.WriteLine(); - writer.Write(e.Exception.ToString()); + //Exception message stacktrace goes as a separate exception property + writer.Write(e.Exception.Message); } } @@ -114,9 +122,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..b8a1fda 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.6 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;net10.0;netstandard2.0 latest enable @@ -29,14 +29,20 @@ - - + + 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..f5f1956 100644 --- a/src/TestWeb/TestWeb.csproj +++ b/src/TestWeb/TestWeb.csproj @@ -1,15 +1,15 @@  - net6.0 + net10.0 default false - - - + + +