From 2eb427c65e5c8d0fc69c10a11c7bb1d097953132 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Fri, 2 Sep 2022 23:20:17 -0400 Subject: [PATCH 1/4] Update deploy.yml --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index efebcb0..f1aa6f3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -30,8 +30,8 @@ jobs: - name: Pack run: dotnet pack -c Release -p:PackageVersion=${{ steps.gitversion.outputs.nuGetVersion }} -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -# && (github.ref_name == 'master') + - name: Publish - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && (github.ref_name == 'master') run: | dotnet nuget push **/*.nupkg --source 'https://api.nuget.org/v3/index.json' -k ${{ secrets.NUGETKEY }} From 169a7349428bd095c3fd638e4bc27d3bcf22e494 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Fri, 2 Sep 2022 23:25:38 -0400 Subject: [PATCH 2/4] Attempting to fix #76 The string comparison was case sensitive... wording in exception is "Collection already exists" -- so it probably won't match. --- src/Serilog.Sinks.MongoDB/Helpers/MongoDbHelpers.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Serilog.Sinks.MongoDB/Helpers/MongoDbHelpers.cs b/src/Serilog.Sinks.MongoDB/Helpers/MongoDbHelpers.cs index ee0576c..e023351 100644 --- a/src/Serilog.Sinks.MongoDB/Helpers/MongoDbHelpers.cs +++ b/src/Serilog.Sinks.MongoDB/Helpers/MongoDbHelpers.cs @@ -58,7 +58,9 @@ internal static void VerifyCollectionExists( { database.CreateCollection(collectionName, collectionCreationOptions); } - catch (MongoCommandException e) when (e.ErrorMessage.Contains("collection already exists")) + catch (MongoCommandException e) when (e.ErrorMessage.Contains( + "collection already exists", + StringComparison.InvariantCultureIgnoreCase)) { // handled } From 65180a0a3b8216dd28b28ee43460d0fe53d4601a Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Fri, 2 Sep 2022 23:37:12 -0400 Subject: [PATCH 3/4] Need a backwards compatible option for Contains() with StringComparison. --- .../Helpers/CollectionHelpers.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/Serilog.Sinks.MongoDB/Helpers/CollectionHelpers.cs diff --git a/src/Serilog.Sinks.MongoDB/Helpers/CollectionHelpers.cs b/src/Serilog.Sinks.MongoDB/Helpers/CollectionHelpers.cs new file mode 100644 index 0000000..4de4a63 --- /dev/null +++ b/src/Serilog.Sinks.MongoDB/Helpers/CollectionHelpers.cs @@ -0,0 +1,16 @@ + + +// ReSharper disable once CheckNamespace +namespace System; +#if !NETSTANDARD2_1 +internal static class CollectionHelpers +{ + internal static bool Contains( + this string? str, + string value, + StringComparison comparison) + { + return str?.IndexOf(value, comparison) >= 0; + } +} +#endif \ No newline at end of file From b7877f731d6cd136dd5908e90a17b0f7237fd0f9 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Fri, 2 Sep 2022 23:46:13 -0400 Subject: [PATCH 4/4] Missing Copyright Switching to file scoped namespace. Changed RollingInterval to use invariant culture for file date formatting. Fixed broken configuration feature for .NET Framework. --- .../Helpers/CollectionHelpers.cs | 16 - .../Helpers/MongoDbDocumentHelpers.cs | 118 ++-- .../Helpers/MongoDbHelpers.cs | 158 +++--- .../Helpers/RollingIntervalHelper.cs | 62 +- .../Helpers/StringHelpers.cs | 28 + .../LoggerConfigurationMongoDBExtensions.cs | 531 +++++++++--------- .../Serilog.Sinks.MongoDB.csproj | 4 + .../Sinks/MongoDB/LogEntry.cs | 55 +- .../Sinks/MongoDB/MongoDBJsonFormatter.cs | 129 +++-- .../Sinks/MongoDB/MongoDBSink.cs | 45 +- .../Sinks/MongoDB/MongoDBSinkBase.cs | 124 ++-- .../Sinks/MongoDB/MongoDBSinkConfiguration.cs | 251 ++++----- .../Sinks/MongoDB/MongoDBSinkDefaults.cs | 41 +- .../Sinks/MongoDB/MongoDBSinkLegacy.cs | 144 +++-- .../Sinks/MongoDB/RollingInterval.cs | 57 +- ...ggerConfigurationMongoDBExtensionsTests.cs | 5 +- .../Serilog.Sinks.MongoDB.Tests.csproj | 1 + 17 files changed, 878 insertions(+), 891 deletions(-) delete mode 100644 src/Serilog.Sinks.MongoDB/Helpers/CollectionHelpers.cs create mode 100644 src/Serilog.Sinks.MongoDB/Helpers/StringHelpers.cs diff --git a/src/Serilog.Sinks.MongoDB/Helpers/CollectionHelpers.cs b/src/Serilog.Sinks.MongoDB/Helpers/CollectionHelpers.cs deleted file mode 100644 index 4de4a63..0000000 --- a/src/Serilog.Sinks.MongoDB/Helpers/CollectionHelpers.cs +++ /dev/null @@ -1,16 +0,0 @@ - - -// ReSharper disable once CheckNamespace -namespace System; -#if !NETSTANDARD2_1 -internal static class CollectionHelpers -{ - internal static bool Contains( - this string? str, - string value, - StringComparison comparison) - { - return str?.IndexOf(value, comparison) >= 0; - } -} -#endif \ No newline at end of file diff --git a/src/Serilog.Sinks.MongoDB/Helpers/MongoDbDocumentHelpers.cs b/src/Serilog.Sinks.MongoDB/Helpers/MongoDbDocumentHelpers.cs index 1004e37..ce63276 100644 --- a/src/Serilog.Sinks.MongoDB/Helpers/MongoDbDocumentHelpers.cs +++ b/src/Serilog.Sinks.MongoDB/Helpers/MongoDbDocumentHelpers.cs @@ -19,79 +19,67 @@ using Serilog.Events; -namespace Serilog.Helpers +namespace Serilog.Helpers; + +internal static class MongoDbDocumentHelpers { - internal static class MongoDbDocumentHelpers + /// + /// Credit to @IvaskevychYuriy for this excellent solution: + /// https://github.com/ChangemakerStudios/serilog-sinks-mongodb/pull/59#issuecomment-830079904 + /// + /// + /// + internal static BsonDocument SanitizeDocumentRecursive(this BsonDocument document) { - /// - /// Credit to @IvaskevychYuriy for this excellent solution: - /// https://github.com/ChangemakerStudios/serilog-sinks-mongodb/pull/59#issuecomment-830079904 - /// - /// - /// - internal static BsonDocument SanitizeDocumentRecursive(this BsonDocument document) - { - if (document == null) throw new ArgumentNullException(nameof(document)); + if (document == null) throw new ArgumentNullException(nameof(document)); - var sanitizedElements = document.Select( - e => new BsonElement( - SanitizedElementName(e.Name), - e.Value.IsBsonDocument - ? SanitizeDocumentRecursive(e.Value.AsBsonDocument) - : e.Value)); + var sanitizedElements = document.Select( + e => new BsonElement( + SanitizedElementName(e.Name), + e.Value.IsBsonDocument + ? SanitizeDocumentRecursive(e.Value.AsBsonDocument) + : e.Value)); - return new BsonDocument(sanitizedElements); - } + return new BsonDocument(sanitizedElements); + } - internal static string SanitizedElementName(this string? name) - { - if (name == null) return "[NULL]"; + internal static string SanitizedElementName(this string? name) + { + if (name == null) return "[NULL]"; - return name.Replace('.', '-').Replace('$', '_'); - } + return name.Replace('.', '-').Replace('$', '_'); + } - internal static BsonValue? ToBsonValue(this LogEventPropertyValue? value) + internal static BsonValue? ToBsonValue(this LogEventPropertyValue? value) + { + if (value == null) return null; + + if (value is ScalarValue scalar) { - if (value == null) return null; - - if (value is ScalarValue scalar) - { - if (scalar.Value is Uri uri) - { - return BsonValue.Create(uri.ToString()); - } - - if (scalar.Value is TimeSpan ts) - { - return BsonValue.Create(ts.ToString()); - } - - if (scalar.Value is DateTimeOffset dto) - { - return BsonValue.Create(dto.ToString()); - } - - return BsonValue.Create(scalar.Value); - } - - if (value is StructureValue sv) - { - return BsonDocument.Create( - sv.Properties.ToDictionary( - s => SanitizedElementName(s.Name), - s => ToBsonValue(s.Value))); - } - - if (value is DictionaryValue dv) - return BsonDocument.Create( - dv.Elements.ToDictionary( - s => SanitizedElementName(s.Key.Value?.ToString()), - s => ToBsonValue(s.Value))); - - if (value is SequenceValue sq) - return BsonValue.Create(sq.Elements.Select(ToBsonValue).ToArray()); - - return null; + if (scalar.Value is Uri uri) return BsonValue.Create(uri.ToString()); + + if (scalar.Value is TimeSpan ts) return BsonValue.Create(ts.ToString()); + + if (scalar.Value is DateTimeOffset dto) return BsonValue.Create(dto.ToString()); + + return BsonValue.Create(scalar.Value); } + + if (value is StructureValue sv) + return BsonDocument.Create( + sv.Properties.ToDictionary( + s => SanitizedElementName(s.Name), + s => ToBsonValue(s.Value))); + + if (value is DictionaryValue dv) + return BsonDocument.Create( + dv.Elements.ToDictionary( + s => SanitizedElementName(s.Key.Value?.ToString()), + s => ToBsonValue(s.Value))); + + if (value is SequenceValue sq) + return BsonValue.Create(sq.Elements.Select(ToBsonValue).ToArray()); + + return null; } } \ No newline at end of file diff --git a/src/Serilog.Sinks.MongoDB/Helpers/MongoDbHelpers.cs b/src/Serilog.Sinks.MongoDB/Helpers/MongoDbHelpers.cs index e023351..67625a7 100644 --- a/src/Serilog.Sinks.MongoDB/Helpers/MongoDbHelpers.cs +++ b/src/Serilog.Sinks.MongoDB/Helpers/MongoDbHelpers.cs @@ -20,103 +20,103 @@ using Serilog.Debugging; using Serilog.Sinks.MongoDB; -namespace Serilog.Helpers +namespace Serilog.Helpers; + +internal static class MongoDbHelper { - internal static class MongoDbHelper + /// + /// Returns true if a collection exists on the mongodb server. + /// + /// The database. + /// Name of the collection. + /// + internal static bool CollectionExists(this IMongoDatabase database, string collectionName) + { + var filter = new BsonDocument("name", collectionName); + var collectionCursor = + database.ListCollections(new ListCollectionsOptions { Filter = filter }); + return collectionCursor.Any(); + } + + /// + /// Verifies the collection exists. If it doesn't, create it using the Collection Creation Options provided. + /// + /// The database. + /// Name of the collection. + /// The collection creation options. + internal static void VerifyCollectionExists( + this IMongoDatabase database, + string collectionName, + CreateCollectionOptions? collectionCreationOptions = null) { - /// - /// Returns true if a collection exists on the mongodb server. - /// - /// The database. - /// Name of the collection. - /// - internal static bool CollectionExists(this IMongoDatabase database, string collectionName) + if (database == null) throw new ArgumentNullException(nameof(database)); + if (collectionName == null) throw new ArgumentNullException(nameof(collectionName)); + + if (database.CollectionExists(collectionName)) return; + + try { - var filter = new BsonDocument("name", collectionName); - var collectionCursor = - database.ListCollections(new ListCollectionsOptions { Filter = filter }); - return collectionCursor.Any(); + database.CreateCollection(collectionName, collectionCreationOptions); } + catch (MongoCommandException e) when (e.ErrorMessage.Contains( + "collection already exists", + StringComparison.InvariantCultureIgnoreCase)) + { + // handled + } + } + + internal static void VerifyExpireTTLSetup( + this IMongoDatabase database, + string collectionName, + TimeSpan? expireTtl) + { + const string ExpireTTLIndexName = "serilog_sink_expired_ttl"; + + var logCollection = database.GetCollection(collectionName); - /// - /// Verifies the collection exists. If it doesn't, create it using the Collection Creation Options provided. - /// - /// The database. - /// Name of the collection. - /// The collection creation options. - internal static void VerifyCollectionExists( - this IMongoDatabase database, - string collectionName, - CreateCollectionOptions? collectionCreationOptions = null) + if (expireTtl.HasValue) { - if (database == null) throw new ArgumentNullException(nameof(database)); - if (collectionName == null) throw new ArgumentNullException(nameof(collectionName)); + var indexKeysDefinition = + Builders.IndexKeys.Ascending(s => s.UtcTimeStamp); + var indexOptions = new CreateIndexOptions + { Name = ExpireTTLIndexName, ExpireAfter = expireTtl }; + var indexModel = new CreateIndexModel(indexKeysDefinition, indexOptions); - if (database.CollectionExists(collectionName)) return; + try + { + logCollection.Indexes.CreateOne(indexModel); + + return; + } + catch (MongoCommandException ex) when (ex.ErrorMessage.Contains( + "already exists with different options")) + { + // handled -- just drop and recreate + logCollection.Indexes.DropOne(ExpireTTLIndexName); + } try { - database.CreateCollection(collectionName, collectionCreationOptions); + // delete the index and re-create since it exists with different expiration value + logCollection.Indexes.CreateOne(indexModel); } - catch (MongoCommandException e) when (e.ErrorMessage.Contains( - "collection already exists", - StringComparison.InvariantCultureIgnoreCase)) + catch (MongoCommandException ex) { - // handled + SelfLog.WriteLine( + "Failure dropping/creating MongoDB Expire TTL Index: {0}", + ex.ErrorMessage); } } - - internal static void VerifyExpireTTLSetup( - this IMongoDatabase database, - string collectionName, - TimeSpan? expireTtl) + else { - const string ExpireTTLIndexName = "serilog_sink_expired_ttl"; - - var logCollection = database.GetCollection(collectionName); - - if (expireTtl.HasValue) + // make sure the expire TTL index doesn't exist + try { - var indexKeysDefinition = - Builders.IndexKeys.Ascending(s => s.UtcTimeStamp); - var indexOptions = new CreateIndexOptions - { Name = ExpireTTLIndexName, ExpireAfter = expireTtl }; - var indexModel = new CreateIndexModel(indexKeysDefinition, indexOptions); - - try - { - logCollection.Indexes.CreateOne(indexModel); - - return; - } - catch (MongoCommandException ex) when (ex.ErrorMessage.Contains("already exists with different options")) - { - // handled -- just drop and recreate - logCollection.Indexes.DropOne(ExpireTTLIndexName); - } - - try - { - // delete the index and re-create since it exists with different expiration value - logCollection.Indexes.CreateOne(indexModel); - } - catch (MongoCommandException ex) - { - SelfLog.WriteLine( - "Failure dropping/creating MongoDB Expire TTL Index: {0}", - ex.ErrorMessage); - } + logCollection.Indexes.DropOne(ExpireTTLIndexName); } - else + catch (MongoCommandException) { - // make sure the expire TTL index doesn't exist - try - { - logCollection.Indexes.DropOne(ExpireTTLIndexName); - } - catch (MongoCommandException) - { - } } } } diff --git a/src/Serilog.Sinks.MongoDB/Helpers/RollingIntervalHelper.cs b/src/Serilog.Sinks.MongoDB/Helpers/RollingIntervalHelper.cs index 87117c7..f5ea6e8 100644 --- a/src/Serilog.Sinks.MongoDB/Helpers/RollingIntervalHelper.cs +++ b/src/Serilog.Sinks.MongoDB/Helpers/RollingIntervalHelper.cs @@ -13,43 +13,45 @@ // limitations under the License. using System; +using System.Globalization; + using Serilog.Sinks.MongoDB; -namespace Serilog.Helpers +namespace Serilog.Helpers; + +internal static class RollingIntervalHelper { - internal static class RollingIntervalHelper + /// + /// Returns collection name based on rolling interval. + /// + /// log202210 + /// The . + /// Collection name. + /// If passed invalid rolling interval. + internal static string GetCollectionName(this RollingInterval interval, string collectionName) { - /// - /// Returns collection name based on rolling interval. - /// - /// log202210 - /// The . - /// Collection name. - /// If passed invalid rolling interval. - internal static string GetCollectionName(this RollingInterval interval, string collectionName) - { - if (interval == RollingInterval.Infinite) return collectionName; + if (interval == RollingInterval.Infinite) return collectionName; - return $"{collectionName}_{DateTime.Now.ToString(GetDateTimeFormatForInterval(interval))}"; - } + return + $"{collectionName}_{DateTime.Now.ToString(GetDateTimeFormatForInterval(interval), CultureInfo.InvariantCulture)}"; + } - internal static string GetDateTimeFormatForInterval(this RollingInterval interval) + internal static string GetDateTimeFormatForInterval(this RollingInterval interval) + { + switch (interval) { - switch (interval) - { - case RollingInterval.Year: - return "yyyy"; - case RollingInterval.Month: - return "yyyyMM"; - case RollingInterval.Day: - return "yyyyMMdd"; - case RollingInterval.Hour: - return "yyyyMMddhh"; - case RollingInterval.Minute: - return "yyyyMMddhhmm"; - default: - throw new ArgumentException("Invalid rolling interval"); - } + case RollingInterval.Year: + return "yyyy"; + case RollingInterval.Month: + return "yyyyMM"; + case RollingInterval.Day: + return "yyyyMMdd"; + case RollingInterval.Hour: + return "yyyyMMddhh"; + case RollingInterval.Minute: + return "yyyyMMddhhmm"; + default: + throw new ArgumentException("Invalid rolling interval"); } } } \ No newline at end of file diff --git a/src/Serilog.Sinks.MongoDB/Helpers/StringHelpers.cs b/src/Serilog.Sinks.MongoDB/Helpers/StringHelpers.cs new file mode 100644 index 0000000..d00a4e5 --- /dev/null +++ b/src/Serilog.Sinks.MongoDB/Helpers/StringHelpers.cs @@ -0,0 +1,28 @@ +// Copyright 2014-2022 Serilog Contributors +// +// 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 +// +// http://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. + +// ReSharper disable once CheckNamespace +namespace System; +#if !NETSTANDARD2_1 +internal static class StringHelpers +{ + internal static bool Contains( + this string? str, + string value, + StringComparison comparison) + { + return str?.IndexOf(value, comparison) >= 0; + } +} +#endif \ No newline at end of file diff --git a/src/Serilog.Sinks.MongoDB/LoggerConfigurationMongoDBExtensions.cs b/src/Serilog.Sinks.MongoDB/LoggerConfigurationMongoDBExtensions.cs index 5c07f77..35a4a69 100644 --- a/src/Serilog.Sinks.MongoDB/LoggerConfigurationMongoDBExtensions.cs +++ b/src/Serilog.Sinks.MongoDB/LoggerConfigurationMongoDBExtensions.cs @@ -22,281 +22,272 @@ using Serilog.Sinks.MongoDB; // ReSharper disable once CheckNamespace -namespace Serilog +namespace Serilog; + +/// +/// Adds the WriteTo.MongoDB() extension method to . +/// +public static class LoggerConfigurationMongoDBExtensions { /// - /// Adds the WriteTo.MongoDB() extension method to . + /// Adds a sink that writes log events as bson documents to a MongoDb database. + /// For AppSettings Configuration. + /// + /// + /// Mongo Url Connection String + /// + /// + /// + /// + /// + /// + /// + /// + public static LoggerConfiguration MongoDBBson( + this LoggerSinkConfiguration loggerConfiguration, + string databaseUrl, + string collectionName = MongoDBSinkDefaults.CollectionName, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + int batchPostingLimit = MongoDBSinkDefaults.BatchPostingLimit, + TimeSpan? period = null, + long? cappedMaxSizeMb = null, + long? cappedMaxDocuments = null, + RollingInterval? rollingInterval = null) + { + var cfg = new MongoDBSinkConfiguration(); + + cfg.SetMongoUrl(databaseUrl); + cfg.SetCollectionName(collectionName); + cfg.SetBatchPostingLimit(batchPostingLimit); + + if (period.HasValue) cfg.SetBatchPeriod(period.Value); + + if (cappedMaxSizeMb.HasValue || cappedMaxDocuments.HasValue) + cfg.SetCreateCappedCollection( + cappedMaxSizeMb ?? MongoDBSinkDefaults.CappedCollectionMaxSizeMb, + cappedMaxDocuments); + + if (rollingInterval.HasValue) cfg.SetRollingInternal(rollingInterval.Value); + + cfg.Validate(); + + return loggerConfiguration.Sink( + new MongoDBSink(cfg), + restrictedToMinimumLevel); + } + + /// + /// Adds a sink that writes log events as bson documents to a MongoDb database. + /// Fluent configuration. + /// + /// + /// + /// + /// + public static LoggerConfiguration MongoDBBson( + this LoggerSinkConfiguration loggerConfiguration, + Action configureAction, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) + { + if (loggerConfiguration == null) + throw new ArgumentNullException(nameof(loggerConfiguration)); + if (configureAction == null) throw new ArgumentNullException(nameof(configureAction)); + + var cfg = new MongoDBSinkConfiguration(); + + configureAction(cfg); + + cfg.Validate(); + + return loggerConfiguration.Sink( + new MongoDBSink(cfg), + restrictedToMinimumLevel); + } + + /// + /// Adds a sink that writes log events as documents to a MongoDb database. + /// + /// The logger configuration. + /// The URL of a created MongoDB collection that log events will be written to. + /// The minimum log event level required in order to write an event to the sink. + /// Name of the collection. Default is "log". + /// The maximum number of events to post in a single batch. + /// The time to wait between checking for event batches. + /// Supplies culture-specific formatting information, or null. + /// Formatter to produce json for MongoDB. + /// Logger configuration, allowing configuration to continue. + /// A required parameter is null. + public static LoggerConfiguration MongoDB( + this LoggerSinkConfiguration loggerConfiguration, + string databaseUrl, + string collectionName = MongoDBSinkDefaults.CollectionName, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + int batchPostingLimit = MongoDBSinkDefaults.BatchPostingLimit, + TimeSpan? period = null, + IFormatProvider? formatProvider = null, + ITextFormatter? mongoDBJsonFormatter = null) + { + if (loggerConfiguration == null) + throw new ArgumentNullException(nameof(loggerConfiguration)); + if (string.IsNullOrWhiteSpace(databaseUrl)) + throw new ArgumentNullException(nameof(databaseUrl)); + + var cfg = new MongoDBSinkConfiguration { Legacy = true }; + + cfg.SetConnectionString(databaseUrl); + cfg.SetBatchPostingLimit(batchPostingLimit); + if (period.HasValue) cfg.SetBatchPeriod(period.Value); + cfg.SetCollectionName(collectionName); + + return + loggerConfiguration.Sink( + new MongoDBSinkLegacy( + cfg, + formatProvider, + mongoDBJsonFormatter), + restrictedToMinimumLevel); + } + + /// + /// Adds a sink that writes log events as documents to a MongoDb database. /// - public static class LoggerConfigurationMongoDBExtensions + /// The logger configuration. + /// The MongoDb database where the log collection will live. + /// The minimum log event level required in order to write an event to the sink. + /// Name of the collection. Default is "log". + /// The maximum number of events to post in a single batch. + /// The time to wait between checking for event batches. + /// Supplies culture-specific formatting information, or null. + /// Formatter to produce json for MongoDB. + /// Logger configuration, allowing configuration to continue. + /// A required parameter is null. + public static LoggerConfiguration MongoDB( + this LoggerSinkConfiguration loggerConfiguration, + IMongoDatabase database, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + string collectionName = MongoDBSinkDefaults.CollectionName, + int batchPostingLimit = MongoDBSinkDefaults.BatchPostingLimit, + TimeSpan? period = null, + IFormatProvider? formatProvider = null, + ITextFormatter? mongoDBJsonFormatter = null) { - /// - /// Adds a sink that writes log events as bson documents to a MongoDb database. - /// For AppSettings Configuration. - /// - /// - /// Mongo Url Connection String - /// - /// - /// - /// - /// - /// - /// - /// - public static LoggerConfiguration MongoDBBson( - this LoggerSinkConfiguration loggerConfiguration, - string databaseUrl, - string collectionName = MongoDBSinkDefaults.CollectionName, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - int batchPostingLimit = MongoDBSinkDefaults.BatchPostingLimit, - TimeSpan? period = null, - long? cappedMaxSizeMb = null, - long? cappedMaxDocuments = null, - RollingInterval? rollingInterval = null) - { - var cfg = new MongoDBSinkConfiguration(); - - cfg.SetMongoUrl(databaseUrl); - cfg.SetCollectionName(collectionName); - cfg.SetBatchPostingLimit(batchPostingLimit); - - if (period.HasValue) - { - cfg.SetBatchPeriod(period.Value); - } - - if (cappedMaxSizeMb.HasValue || cappedMaxDocuments.HasValue) - { - cfg.SetCreateCappedCollection( - cappedMaxSizeMb ?? MongoDBSinkDefaults.CappedCollectionMaxSizeMb, - cappedMaxDocuments); - } - - if (rollingInterval.HasValue) - { - cfg.SetRollingInternal(rollingInterval.Value); - } - - cfg.Validate(); - - return loggerConfiguration.Sink( - new MongoDBSink(cfg), + if (loggerConfiguration == null) + throw new ArgumentNullException(nameof(loggerConfiguration)); + if (database == null) throw new ArgumentNullException(nameof(database)); + + var cfg = new MongoDBSinkConfiguration { Legacy = true }; + + cfg.SetMongoDatabase(database); + cfg.SetBatchPostingLimit(batchPostingLimit); + if (period.HasValue) cfg.SetBatchPeriod(period.Value); + cfg.SetCollectionName(collectionName); + + return + loggerConfiguration.Sink( + new MongoDBSinkLegacy( + cfg, + formatProvider, + mongoDBJsonFormatter), restrictedToMinimumLevel); - } - - /// - /// Adds a sink that writes log events as bson documents to a MongoDb database. - /// Fluent configuration. - /// - /// - /// - /// - /// - public static LoggerConfiguration MongoDBBson( - this LoggerSinkConfiguration loggerConfiguration, - Action configureAction, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) - { - if (loggerConfiguration == null) - throw new ArgumentNullException(nameof(loggerConfiguration)); - if (configureAction == null) throw new ArgumentNullException(nameof(configureAction)); - - var cfg = new MongoDBSinkConfiguration(); - - configureAction(cfg); - - cfg.Validate(); - - return loggerConfiguration.Sink( - new MongoDBSink(cfg), + } + + /// + /// Adds a sink that writes log events as documents to a capped collection in a MongoDb database. + /// + /// The logger configuration. + /// + /// The URL of a MongoDb database where the log collection will live (used for backwards + /// compatibility). + /// + /// The minimum log event level required in order to write an event to the sink. + /// Max total size in megabytes of the created capped collection. (Default: 50mb) + /// Max number of documents of the created capped collection. + /// Name of the collection. Default is "log". + /// The maximum number of events to post in a single batch. + /// The time to wait between checking for event batches. + /// Supplies culture-specific formatting information, or null. + /// Formatter to produce json for MongoDB. + /// Logger configuration, allowing configuration to continue. + /// A required parameter is null. + public static LoggerConfiguration MongoDBCapped( + this LoggerSinkConfiguration loggerConfiguration, + string databaseUrl, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + long cappedMaxSizeMb = 50, + long? cappedMaxDocuments = null, + string collectionName = MongoDBSinkDefaults.CollectionName, + int batchPostingLimit = MongoDBSinkDefaults.BatchPostingLimit, + TimeSpan? period = null, + IFormatProvider? formatProvider = null, + ITextFormatter? mongoDBJsonFormatter = null) + { + if (loggerConfiguration == null) + throw new ArgumentNullException(nameof(loggerConfiguration)); + if (string.IsNullOrWhiteSpace(databaseUrl)) + throw new ArgumentNullException(nameof(databaseUrl)); + + var cfg = new MongoDBSinkConfiguration { Legacy = true }; + + cfg.SetConnectionString(databaseUrl); + cfg.SetBatchPostingLimit(batchPostingLimit); + if (period.HasValue) cfg.SetBatchPeriod(period.Value); + cfg.SetCollectionName(collectionName); + cfg.SetCreateCappedCollection(cappedMaxSizeMb, cappedMaxDocuments); + + return + loggerConfiguration.Sink( + new MongoDBSinkLegacy( + cfg, + formatProvider, + mongoDBJsonFormatter), + restrictedToMinimumLevel); + } + + /// + /// Adds a sink that writes log events as documents to a capped collection in a MongoDb database. + /// + /// The logger configuration. + /// The MongoDb database where the log collection will live. + /// The minimum log event level required in order to write an event to the sink. + /// Max total size in megabytes of the created capped collection. (Default: 50mb) + /// Max number of documents of the created capped collection. + /// Name of the collection. Default is "log". + /// The maximum number of events to post in a single batch. + /// The time to wait between checking for event batches. + /// Supplies culture-specific formatting information, or null. + /// Formatter to produce json for MongoDB. + /// Logger configuration, allowing configuration to continue. + /// A required parameter is null. + public static LoggerConfiguration MongoDBCapped( + this LoggerSinkConfiguration loggerConfiguration, + IMongoDatabase database, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + long cappedMaxSizeMb = 50, + long? cappedMaxDocuments = null, + string collectionName = MongoDBSinkDefaults.CollectionName, + int batchPostingLimit = MongoDBSinkDefaults.BatchPostingLimit, + TimeSpan? period = null, + IFormatProvider? formatProvider = null, + ITextFormatter? mongoDBJsonFormatter = null) + { + if (loggerConfiguration == null) + throw new ArgumentNullException(nameof(loggerConfiguration)); + if (database == null) throw new ArgumentNullException(nameof(database)); + + var cfg = new MongoDBSinkConfiguration { Legacy = true }; + + cfg.SetMongoDatabase(database); + cfg.SetBatchPostingLimit(batchPostingLimit); + if (period.HasValue) cfg.SetBatchPeriod(period.Value); + cfg.SetCollectionName(collectionName); + cfg.SetCreateCappedCollection(cappedMaxSizeMb, cappedMaxDocuments); + + return + loggerConfiguration.Sink( + new MongoDBSinkLegacy( + cfg, + formatProvider, + mongoDBJsonFormatter), restrictedToMinimumLevel); - } - - /// - /// Adds a sink that writes log events as documents to a MongoDb database. - /// - /// The logger configuration. - /// The URL of a created MongoDB collection that log events will be written to. - /// The minimum log event level required in order to write an event to the sink. - /// Name of the collection. Default is "log". - /// The maximum number of events to post in a single batch. - /// The time to wait between checking for event batches. - /// Supplies culture-specific formatting information, or null. - /// Formatter to produce json for MongoDB. - /// Logger configuration, allowing configuration to continue. - /// A required parameter is null. - public static LoggerConfiguration MongoDB( - this LoggerSinkConfiguration loggerConfiguration, - string databaseUrl, - string collectionName = MongoDBSinkDefaults.CollectionName, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - int batchPostingLimit = MongoDBSinkDefaults.BatchPostingLimit, - TimeSpan? period = null, - IFormatProvider? formatProvider = null, - ITextFormatter? mongoDBJsonFormatter = null) - { - if (loggerConfiguration == null) - throw new ArgumentNullException(nameof(loggerConfiguration)); - if (string.IsNullOrWhiteSpace(databaseUrl)) - throw new ArgumentNullException(nameof(databaseUrl)); - - var cfg = new MongoDBSinkConfiguration() { Legacy = true }; - - cfg.SetConnectionString(databaseUrl); - cfg.SetBatchPostingLimit(batchPostingLimit); - if (period.HasValue) cfg.SetBatchPeriod(period.Value); - cfg.SetCollectionName(collectionName); - - return - loggerConfiguration.Sink( - new MongoDBSinkLegacy( - cfg, - formatProvider, - mongoDBJsonFormatter), - restrictedToMinimumLevel); - } - - /// - /// Adds a sink that writes log events as documents to a MongoDb database. - /// - /// The logger configuration. - /// The MongoDb database where the log collection will live. - /// The minimum log event level required in order to write an event to the sink. - /// Name of the collection. Default is "log". - /// The maximum number of events to post in a single batch. - /// The time to wait between checking for event batches. - /// Supplies culture-specific formatting information, or null. - /// Formatter to produce json for MongoDB. - /// Logger configuration, allowing configuration to continue. - /// A required parameter is null. - public static LoggerConfiguration MongoDB( - this LoggerSinkConfiguration loggerConfiguration, - IMongoDatabase database, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - string collectionName = MongoDBSinkDefaults.CollectionName, - int batchPostingLimit = MongoDBSinkDefaults.BatchPostingLimit, - TimeSpan? period = null, - IFormatProvider? formatProvider = null, - ITextFormatter? mongoDBJsonFormatter = null) - { - if (loggerConfiguration == null) - throw new ArgumentNullException(nameof(loggerConfiguration)); - if (database == null) throw new ArgumentNullException(nameof(database)); - - var cfg = new MongoDBSinkConfiguration() { Legacy = true }; - - cfg.SetMongoDatabase(database); - cfg.SetBatchPostingLimit(batchPostingLimit); - if (period.HasValue) cfg.SetBatchPeriod(period.Value); - cfg.SetCollectionName(collectionName); - - return - loggerConfiguration.Sink( - new MongoDBSinkLegacy( - cfg, - formatProvider, - mongoDBJsonFormatter), - restrictedToMinimumLevel); - } - - /// - /// Adds a sink that writes log events as documents to a capped collection in a MongoDb database. - /// - /// The logger configuration. - /// - /// The URL of a MongoDb database where the log collection will live (used for backwards - /// compatibility). - /// - /// The minimum log event level required in order to write an event to the sink. - /// Max total size in megabytes of the created capped collection. (Default: 50mb) - /// Max number of documents of the created capped collection. - /// Name of the collection. Default is "log". - /// The maximum number of events to post in a single batch. - /// The time to wait between checking for event batches. - /// Supplies culture-specific formatting information, or null. - /// Formatter to produce json for MongoDB. - /// Logger configuration, allowing configuration to continue. - /// A required parameter is null. - public static LoggerConfiguration MongoDBCapped( - this LoggerSinkConfiguration loggerConfiguration, - string databaseUrl, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - long cappedMaxSizeMb = 50, - long? cappedMaxDocuments = null, - string collectionName = MongoDBSinkDefaults.CollectionName, - int batchPostingLimit = MongoDBSinkDefaults.BatchPostingLimit, - TimeSpan? period = null, - IFormatProvider? formatProvider = null, - ITextFormatter? mongoDBJsonFormatter = null) - { - if (loggerConfiguration == null) - throw new ArgumentNullException(nameof(loggerConfiguration)); - if (string.IsNullOrWhiteSpace(databaseUrl)) - throw new ArgumentNullException(nameof(databaseUrl)); - - var cfg = new MongoDBSinkConfiguration() { Legacy = true }; - - cfg.SetConnectionString(databaseUrl); - cfg.SetBatchPostingLimit(batchPostingLimit); - if (period.HasValue) cfg.SetBatchPeriod(period.Value); - cfg.SetCollectionName(collectionName); - cfg.SetCreateCappedCollection(cappedMaxSizeMb, cappedMaxDocuments); - - return - loggerConfiguration.Sink( - new MongoDBSinkLegacy( - cfg, - formatProvider, - mongoDBJsonFormatter), - restrictedToMinimumLevel); - } - - /// - /// Adds a sink that writes log events as documents to a capped collection in a MongoDb database. - /// - /// The logger configuration. - /// The MongoDb database where the log collection will live. - /// The minimum log event level required in order to write an event to the sink. - /// Max total size in megabytes of the created capped collection. (Default: 50mb) - /// Max number of documents of the created capped collection. - /// Name of the collection. Default is "log". - /// The maximum number of events to post in a single batch. - /// The time to wait between checking for event batches. - /// Supplies culture-specific formatting information, or null. - /// Formatter to produce json for MongoDB. - /// Logger configuration, allowing configuration to continue. - /// A required parameter is null. - public static LoggerConfiguration MongoDBCapped( - this LoggerSinkConfiguration loggerConfiguration, - IMongoDatabase database, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - long cappedMaxSizeMb = 50, - long? cappedMaxDocuments = null, - string collectionName = MongoDBSinkDefaults.CollectionName, - int batchPostingLimit = MongoDBSinkDefaults.BatchPostingLimit, - TimeSpan? period = null, - IFormatProvider? formatProvider = null, - ITextFormatter? mongoDBJsonFormatter = null) - { - if (loggerConfiguration == null) - throw new ArgumentNullException(nameof(loggerConfiguration)); - if (database == null) throw new ArgumentNullException(nameof(database)); - - var cfg = new MongoDBSinkConfiguration() { Legacy = true }; - - cfg.SetMongoDatabase(database); - cfg.SetBatchPostingLimit(batchPostingLimit); - if (period.HasValue) cfg.SetBatchPeriod(period.Value); - cfg.SetCollectionName(collectionName); - cfg.SetCreateCappedCollection(cappedMaxSizeMb, cappedMaxDocuments); - - return - loggerConfiguration.Sink( - new MongoDBSinkLegacy( - cfg, - formatProvider, - mongoDBJsonFormatter), - restrictedToMinimumLevel); - } } } \ No newline at end of file diff --git a/src/Serilog.Sinks.MongoDB/Serilog.Sinks.MongoDB.csproj b/src/Serilog.Sinks.MongoDB/Serilog.Sinks.MongoDB.csproj index 35ffe66..21cefca 100644 --- a/src/Serilog.Sinks.MongoDB/Serilog.Sinks.MongoDB.csproj +++ b/src/Serilog.Sinks.MongoDB/Serilog.Sinks.MongoDB.csproj @@ -40,6 +40,10 @@ + + + + diff --git a/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/LogEntry.cs b/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/LogEntry.cs index 6f07950..e78458d 100644 --- a/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/LogEntry.cs +++ b/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/LogEntry.cs @@ -21,42 +21,41 @@ using Serilog.Events; using Serilog.Helpers; -namespace Serilog.Sinks.MongoDB +namespace Serilog.Sinks.MongoDB; + +public class LogEntry { - public class LogEntry - { - [BsonRepresentation(BsonType.ObjectId)] - public string? Id { get; set; } + [BsonRepresentation(BsonType.ObjectId)] + public string? Id { get; set; } - [BsonRepresentation(BsonType.String)] - public LogEventLevel Level { get; set; } + [BsonRepresentation(BsonType.String)] + public LogEventLevel Level { get; set; } - public DateTime UtcTimeStamp { get; set; } + public DateTime UtcTimeStamp { get; set; } - public MessageTemplate? MessageTemplate { get; set; } + public MessageTemplate? MessageTemplate { get; set; } - public string? RenderedMessage { get; set; } + public string? RenderedMessage { get; set; } - public BsonDocument? Properties { get; set; } + public BsonDocument? Properties { get; set; } - public BsonDocument? Exception { get; set; } + public BsonDocument? Exception { get; set; } + + public static LogEntry MapFrom(LogEvent logEvent) + { + if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); - public static LogEntry MapFrom(LogEvent logEvent) + return new LogEntry { - if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); - - return new LogEntry - { - MessageTemplate = logEvent.MessageTemplate, - RenderedMessage = logEvent.RenderMessage(), - Level = logEvent.Level, - UtcTimeStamp = logEvent.Timestamp.ToUniversalTime().UtcDateTime, - Exception = logEvent.Exception?.ToBsonDocument().SanitizeDocumentRecursive(), - Properties = BsonDocument.Create( - logEvent.Properties.ToDictionary( - s => s.Key.SanitizedElementName(), - s => s.Value.ToBsonValue())) - }; - } + MessageTemplate = logEvent.MessageTemplate, + RenderedMessage = logEvent.RenderMessage(), + Level = logEvent.Level, + UtcTimeStamp = logEvent.Timestamp.ToUniversalTime().UtcDateTime, + Exception = logEvent.Exception?.ToBsonDocument().SanitizeDocumentRecursive(), + Properties = BsonDocument.Create( + logEvent.Properties.ToDictionary( + s => s.Key.SanitizedElementName(), + s => s.Value.ToBsonValue())) + }; } } \ No newline at end of file diff --git a/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBJsonFormatter.cs b/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBJsonFormatter.cs index 01be606..593c9cc 100644 --- a/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBJsonFormatter.cs +++ b/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBJsonFormatter.cs @@ -20,78 +20,83 @@ using Serilog.Formatting.Json; -namespace Serilog.Sinks.MongoDB +namespace Serilog.Sinks.MongoDB; + +/// +/// JsonFormatter for MongoDB +/// +public class MongoDBJsonFormatter : JsonFormatter { + private readonly IDictionary> _dateTimeWriters; + /// - /// JsonFormatter for MongoDB + /// See . + /// Default JsonFormatter writes DateTimeOffset as string with round trip date/time pattern to MongoDB, + /// which contains the local time zone component. String search on these field, with different timezones, + /// will not return the correct values. This JsonFormatter writes them as MongoDB Date objects. /// - public class MongoDBJsonFormatter : JsonFormatter + /// + /// If true, the properties of the event will be written to + /// the output without enclosing braces. Otherwise, if false, each event will be written as a well-formed + /// JSON object. + /// + /// + /// A string that will be written after each log event is formatted. + /// If null, will be used. Ignored if + /// is true. + /// + /// + /// If true, the message will be rendered and written to the output as a + /// property named RenderedMessage. + /// + /// Supplies culture-specific formatting information, or null. + public MongoDBJsonFormatter( + bool omitEnclosingObject = false, + string? closingDelimiter = null, + bool renderMessage = false, + IFormatProvider? formatProvider = null) + : base(omitEnclosingObject, closingDelimiter, renderMessage, formatProvider) { - private readonly IDictionary> _dateTimeWriters; - - /// - /// See . - /// Default JsonFormatter writes DateTimeOffset as string with round trip date/time pattern to MongoDB, - /// which contains the local time zone component. String search on these field, with different timezones, - /// will not return the correct values. This JsonFormatter writes them as MongoDB Date objects. - /// - /// If true, the properties of the event will be written to - /// the output without enclosing braces. Otherwise, if false, each event will be written as a well-formed - /// JSON object. - /// A string that will be written after each log event is formatted. - /// If null, will be used. Ignored if - /// is true. - /// If true, the message will be rendered and written to the output as a - /// property named RenderedMessage. - /// Supplies culture-specific formatting information, or null. - public MongoDBJsonFormatter( - bool omitEnclosingObject = false, - string? closingDelimiter = null, - bool renderMessage = false, - IFormatProvider? formatProvider = null) - : base(omitEnclosingObject, closingDelimiter, renderMessage, formatProvider) + this._dateTimeWriters = new Dictionary> { - this._dateTimeWriters = new Dictionary> - { - {typeof (DateTime), (v, w) => WriteDateTime((DateTime) v, w)}, - {typeof (DateTimeOffset), (v, w) => WriteOffset((DateTimeOffset) v, w)} - }; - } - - /// - /// Writes out a json property with the specified value on output writer - /// - protected override void WriteJsonProperty( - string name, - object? value, - ref string precedingDelimiter, - TextWriter output) - { - name = name.Replace('$', '_').Replace('.', '-'); + { typeof(DateTime), (v, w) => WriteDateTime((DateTime)v, w) }, + { typeof(DateTimeOffset), (v, w) => WriteOffset((DateTimeOffset)v, w) } + }; + } - if (value != null && this._dateTimeWriters.TryGetValue(value.GetType(), out var action)) - { - output.Write(precedingDelimiter); - output.Write("\""); - output.Write(name); - output.Write("\":"); - action(value, output); - precedingDelimiter = ","; - } - else - { - base.WriteJsonProperty(name, value, ref precedingDelimiter, output); - } - } + /// + /// Writes out a json property with the specified value on output writer + /// + protected override void WriteJsonProperty( + string name, + object? value, + ref string precedingDelimiter, + TextWriter output) + { + name = name.Replace('$', '_').Replace('.', '-'); - private static void WriteOffset(DateTimeOffset value, TextWriter output) + if (value != null && this._dateTimeWriters.TryGetValue(value.GetType(), out var action)) { - output.Write($"{{ \"$date\" : {BsonUtils.ToMillisecondsSinceEpoch(value.UtcDateTime)} }}"); + output.Write(precedingDelimiter); + output.Write("\""); + output.Write(name); + output.Write("\":"); + action(value, output); + precedingDelimiter = ","; } - - private static void WriteDateTime(DateTime value, TextWriter output) + else { - output.Write($"{{ \"$date\" : {BsonUtils.ToMillisecondsSinceEpoch(value)} }}"); + base.WriteJsonProperty(name, value, ref precedingDelimiter, output); } } + + private static void WriteOffset(DateTimeOffset value, TextWriter output) + { + output.Write($"{{ \"$date\" : {BsonUtils.ToMillisecondsSinceEpoch(value.UtcDateTime)} }}"); + } + + private static void WriteDateTime(DateTime value, TextWriter output) + { + output.Write($"{{ \"$date\" : {BsonUtils.ToMillisecondsSinceEpoch(value)} }}"); + } } \ No newline at end of file diff --git a/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSink.cs b/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSink.cs index fdb0002..b3f3bf8 100644 --- a/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSink.cs +++ b/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSink.cs @@ -21,32 +21,31 @@ using Serilog.Events; -namespace Serilog.Sinks.MongoDB +namespace Serilog.Sinks.MongoDB; + +public class MongoDBSink : MongoDBSinkBase { - public class MongoDBSink : MongoDBSinkBase + static MongoDBSink() { - static MongoDBSink() - { - if (!BsonClassMap.IsClassMapRegistered(typeof(Exception))) - BsonClassMap.RegisterClassMap( - cm => - { - cm.AutoMap(); - cm.MapProperty(s => s.Message); - cm.MapProperty(s => s.Source); - cm.MapProperty(s => s.StackTrace); - cm.MapProperty(s => s.Data); - }); - } + if (!BsonClassMap.IsClassMapRegistered(typeof(Exception))) + BsonClassMap.RegisterClassMap( + cm => + { + cm.AutoMap(); + cm.MapProperty(s => s.Message); + cm.MapProperty(s => s.Source); + cm.MapProperty(s => s.StackTrace); + cm.MapProperty(s => s.Data); + }); + } - public MongoDBSink(MongoDBSinkConfiguration configuration) - : base(configuration) - { - } + public MongoDBSink(MongoDBSinkConfiguration configuration) + : base(configuration) + { + } - protected override Task EmitBatchAsync(IEnumerable events) - { - return this.InsertMany(events.Select(LogEntry.MapFrom)); - } + protected override Task EmitBatchAsync(IEnumerable events) + { + return this.InsertMany(events.Select(LogEntry.MapFrom)); } } \ No newline at end of file diff --git a/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSinkBase.cs b/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSinkBase.cs index e700971..48d2efe 100644 --- a/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSinkBase.cs +++ b/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSinkBase.cs @@ -22,71 +22,71 @@ using Serilog.Helpers; using Serilog.Sinks.PeriodicBatching; -namespace Serilog.Sinks.MongoDB +namespace Serilog.Sinks.MongoDB; + +/// +/// Writes log events as documents to a MongoDb database. +/// +public abstract class MongoDBSinkBase : PeriodicBatchingSink { + private readonly MongoDBSinkConfiguration _configuration; + + private readonly Lazy _mongoDatabase; + + /// + /// Construct a sink posting to a specified database. + /// + protected MongoDBSinkBase(MongoDBSinkConfiguration configuration) + : base(configuration.BatchPostingLimit, configuration.BatchPeriod) + { + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + + this._configuration = configuration; + + // validate the settings + configuration.Validate(); + + this._mongoDatabase = new Lazy( + () => GetVerifiedMongoDatabaseFromConfiguration(this._configuration), + LazyThreadSafetyMode.ExecutionAndPublication); + } + + protected string CollectionName => this._configuration.CollectionName; + + protected RollingInterval RollingInterval => this._configuration.RollingInterval; + + protected static IMongoDatabase GetVerifiedMongoDatabaseFromConfiguration( + MongoDBSinkConfiguration configuration) + { + var mongoDatabase = configuration.MongoDatabase + ?? new MongoClient(configuration.MongoUrl).GetDatabase( + configuration.MongoUrl!.DatabaseName); + + // connection attempt + mongoDatabase.VerifyCollectionExists( + configuration.CollectionName, + configuration.CollectionCreationOptions); + + // setup TTL if desired + mongoDatabase.VerifyExpireTTLSetup( + configuration.CollectionName, + configuration.ExpireTTL); + + return mongoDatabase; + } + /// - /// Writes log events as documents to a MongoDb database. + /// Gets the log collection. /// - public abstract class MongoDBSinkBase : PeriodicBatchingSink + /// + public IMongoCollection GetCollection() + { + var collectionName = this.RollingInterval.GetCollectionName(this.CollectionName); + return this._mongoDatabase.Value.GetCollection(collectionName); + } + + protected Task InsertMany(IEnumerable objects) { - private readonly MongoDBSinkConfiguration _configuration; - - private readonly Lazy _mongoDatabase; - - /// - /// Construct a sink posting to a specified database. - /// - protected MongoDBSinkBase(MongoDBSinkConfiguration configuration) - : base(configuration.BatchPostingLimit, configuration.BatchPeriod) - { - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - - this._configuration = configuration; - - // validate the settings - configuration.Validate(); - - this._mongoDatabase = new Lazy( - () => GetVerifiedMongoDatabaseFromConfiguration(this._configuration), - LazyThreadSafetyMode.ExecutionAndPublication); - } - - protected string CollectionName => _configuration.CollectionName; - - protected RollingInterval RollingInterval => _configuration.RollingInterval; - protected static IMongoDatabase GetVerifiedMongoDatabaseFromConfiguration( - MongoDBSinkConfiguration configuration) - { - var mongoDatabase = configuration.MongoDatabase - ?? new MongoClient(configuration.MongoUrl).GetDatabase( - configuration.MongoUrl!.DatabaseName); - - // connection attempt - mongoDatabase.VerifyCollectionExists( - configuration.CollectionName, - configuration.CollectionCreationOptions); - - // setup TTL if desired - mongoDatabase.VerifyExpireTTLSetup( - configuration.CollectionName, - configuration.ExpireTTL); - - return mongoDatabase; - } - - /// - /// Gets the log collection. - /// - /// - public IMongoCollection GetCollection() - { - var collectionName = RollingInterval.GetCollectionName(CollectionName); - return this._mongoDatabase.Value.GetCollection(collectionName); - } - - protected Task InsertMany(IEnumerable objects) - { - return Task.WhenAll(this.GetCollection().InsertManyAsync(objects)); - } + return Task.WhenAll(this.GetCollection().InsertManyAsync(objects)); } } \ No newline at end of file diff --git a/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSinkConfiguration.cs b/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSinkConfiguration.cs index bd10bc0..533d541 100644 --- a/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSinkConfiguration.cs +++ b/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSinkConfiguration.cs @@ -16,146 +16,140 @@ using MongoDB.Driver; -namespace Serilog.Sinks.MongoDB +namespace Serilog.Sinks.MongoDB; + +public class MongoDBSinkConfiguration { - public class MongoDBSinkConfiguration - { - public string CollectionName { get; private set; } = MongoDBSinkDefaults.CollectionName; + public string CollectionName { get; private set; } = MongoDBSinkDefaults.CollectionName; - public int BatchPostingLimit { get; private set; } = MongoDBSinkDefaults.BatchPostingLimit; + public int BatchPostingLimit { get; private set; } = MongoDBSinkDefaults.BatchPostingLimit; - public CreateCollectionOptions? CollectionCreationOptions { get; private set; } + public CreateCollectionOptions? CollectionCreationOptions { get; private set; } - public TimeSpan? ExpireTTL { get; private set; } + public TimeSpan? ExpireTTL { get; private set; } - public TimeSpan BatchPeriod { get; private set; } = MongoDBSinkDefaults.BatchPeriod; + public TimeSpan BatchPeriod { get; private set; } = MongoDBSinkDefaults.BatchPeriod; - public MongoUrl? MongoUrl { get; private set; } + public MongoUrl? MongoUrl { get; private set; } - public IMongoDatabase? MongoDatabase { get; private set; } + public IMongoDatabase? MongoDatabase { get; private set; } - public bool Legacy { get; internal set; } + public bool Legacy { get; internal set; } - public RollingInterval RollingInterval { get; private set; } = RollingInterval.Infinite; + public RollingInterval RollingInterval { get; private set; } = RollingInterval.Infinite; - public void Validate() - { - if (MongoDatabase == null && MongoUrl == null) - { - throw new ArgumentOutOfRangeException( - "MongoDatabase or MongoUrl", - "Invalid Configuration: MongoDatabase or Mongo Connection String must be specified."); - } - - if (MongoUrl != null && string.IsNullOrWhiteSpace(MongoUrl.DatabaseName)) - { - throw new ArgumentNullException( - nameof(MongoUrl.DatabaseName), - "Database name is required in the MongoDb connection string. Use: mongodb://mongoDbServer/databaseName"); - } - - if (ExpireTTL.HasValue && Legacy) - { - throw new ArgumentNullException( - nameof(ExpireTTL), - "Expiration TTL is only supported on the MongoDBBson Sink"); - } - } + public void Validate() + { + if (this.MongoDatabase == null && this.MongoUrl == null) + throw new ArgumentOutOfRangeException( + "MongoDatabase or MongoUrl", + "Invalid Configuration: MongoDatabase or Mongo Connection String must be specified."); + + if (this.MongoUrl != null && string.IsNullOrWhiteSpace(this.MongoUrl.DatabaseName)) + throw new ArgumentNullException( + nameof(this.MongoUrl.DatabaseName), + "Database name is required in the MongoDb connection string. Use: mongodb://mongoDbServer/databaseName"); + + if (this.ExpireTTL.HasValue && this.Legacy) + throw new ArgumentNullException( + nameof(this.ExpireTTL), + "Expiration TTL is only supported on the MongoDBBson Sink"); + } - /// - /// Set the RollingInterval. (Default: RollingInterval.Infinite) - /// - /// - public void SetRollingInternal(RollingInterval rollingInterval) - { - RollingInterval = rollingInterval; - } + /// + /// Set the RollingInterval. (Default: RollingInterval.Infinite) + /// + /// + public void SetRollingInternal(RollingInterval rollingInterval) + { + this.RollingInterval = rollingInterval; + } - /// - /// Set the periodic batch timeout period. (Default: 2 seconds) - /// - /// - public void SetBatchPeriod(TimeSpan period) - { - this.BatchPeriod = period; - } + /// + /// Set the periodic batch timeout period. (Default: 2 seconds) + /// + /// + public void SetBatchPeriod(TimeSpan period) + { + this.BatchPeriod = period; + } - /// - /// Sets the expiration time on all log documents: https://docs.mongodb.com/manual/tutorial/expire-data/ - /// Only supported for the MongoDBBson sink. - /// - /// - public void SetExpireTTL(TimeSpan? timeToLive) - { - this.ExpireTTL = timeToLive; - } + /// + /// Sets the expiration time on all log documents: https://docs.mongodb.com/manual/tutorial/expire-data/ + /// Only supported for the MongoDBBson sink. + /// + /// + public void SetExpireTTL(TimeSpan? timeToLive) + { + this.ExpireTTL = timeToLive; + } - /// - /// Setup capped collections during collection creation - /// - /// (Optional) Max Size in Mb of the Capped Collection. Default is 50mb. - /// (Optional) Max Number of Documents in the Capped Collection. Default is none. - public void SetCreateCappedCollection( - long cappedMaxSizeMb = MongoDBSinkDefaults.CappedCollectionMaxSizeMb, - long? cappedMaxDocuments = null) + /// + /// Setup capped collections during collection creation + /// + /// (Optional) Max Size in Mb of the Capped Collection. Default is 50mb. + /// (Optional) Max Number of Documents in the Capped Collection. Default is none. + public void SetCreateCappedCollection( + long cappedMaxSizeMb = MongoDBSinkDefaults.CappedCollectionMaxSizeMb, + long? cappedMaxDocuments = null) + { + this.CollectionCreationOptions = new CreateCollectionOptions { - this.CollectionCreationOptions = new CreateCollectionOptions - { - Capped = true, - MaxSize = cappedMaxSizeMb * 1024 * 1024 - }; - - if (cappedMaxDocuments.HasValue) - this.CollectionCreationOptions.MaxDocuments = cappedMaxDocuments.Value; - } + Capped = true, + MaxSize = cappedMaxSizeMb * 1024 * 1024 + }; - /// - /// Set the mongo database instance directly - /// - /// - public void SetMongoDatabase(IMongoDatabase database) - { - this.MongoDatabase = database ?? throw new ArgumentNullException(nameof(database)); - this.MongoUrl = null; - } + if (cappedMaxDocuments.HasValue) + this.CollectionCreationOptions.MaxDocuments = cappedMaxDocuments.Value; + } - /// - /// Set the mongo url (connection string) -- e.g. mongodb://localhost/databaseName - /// - /// - public void SetMongoUrl(string mongoUrl) - { - if (string.IsNullOrWhiteSpace(mongoUrl)) - throw new ArgumentNullException(nameof(mongoUrl)); + /// + /// Set the mongo database instance directly + /// + /// + public void SetMongoDatabase(IMongoDatabase database) + { + this.MongoDatabase = database ?? throw new ArgumentNullException(nameof(database)); + this.MongoUrl = null; + } - this.MongoUrl = MongoUrl.Create(mongoUrl); - this.MongoDatabase = null; - } + /// + /// Set the mongo url (connection string) -- e.g. mongodb://localhost/databaseName + /// + /// + public void SetMongoUrl(string mongoUrl) + { + if (string.IsNullOrWhiteSpace(mongoUrl)) + throw new ArgumentNullException(nameof(mongoUrl)); - /// - /// Set the MongoDB collection name (Default: 'logs') - /// - /// - public void SetCollectionName(string collectionName) - { - if (collectionName == string.Empty) - { - throw new ArgumentOutOfRangeException(nameof(collectionName), "Must not be string.empty"); - } + this.MongoUrl = MongoUrl.Create(mongoUrl); + this.MongoDatabase = null; + } - CollectionName = collectionName ?? MongoDBSinkDefaults.CollectionName; - } + /// + /// Set the MongoDB collection name (Default: 'logs') + /// + /// + public void SetCollectionName(string collectionName) + { + if (collectionName == string.Empty) + throw new ArgumentOutOfRangeException( + nameof(collectionName), + "Must not be string.empty"); - /// - /// Set the batch posting limit (Default: 50) - /// - /// - public void SetBatchPostingLimit(int batchPostingLimit) - { - this.BatchPostingLimit = batchPostingLimit; - } + this.CollectionName = collectionName ?? MongoDBSinkDefaults.CollectionName; + } + + /// + /// Set the batch posting limit (Default: 50) + /// + /// + public void SetBatchPostingLimit(int batchPostingLimit) + { + this.BatchPostingLimit = batchPostingLimit; + } -#if NET452 +#if NET472 /// /// Tries to set the Mongo url from a connection string in the .config file. /// @@ -190,17 +184,16 @@ public void SetConnectionString(string connectionString) this.SetMongoUrl(connectionString); } #else - /// - /// Set the Mongo url (connection string) - /// - /// - public void SetConnectionString(string connectionString) - { - if (string.IsNullOrWhiteSpace(connectionString)) - throw new ArgumentNullException(nameof(connectionString)); + /// + /// Set the Mongo url (connection string) + /// + /// + public void SetConnectionString(string connectionString) + { + if (string.IsNullOrWhiteSpace(connectionString)) + throw new ArgumentNullException(nameof(connectionString)); - this.SetMongoUrl(connectionString); - } -#endif + this.SetMongoUrl(connectionString); } +#endif } \ No newline at end of file diff --git a/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSinkDefaults.cs b/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSinkDefaults.cs index b6ae57e..a2717aa 100644 --- a/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSinkDefaults.cs +++ b/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSinkDefaults.cs @@ -14,29 +14,28 @@ using System; -namespace Serilog.Sinks.MongoDB +namespace Serilog.Sinks.MongoDB; + +public static class MongoDBSinkDefaults { - public static class MongoDBSinkDefaults - { - /// - /// A reasonable default for the number of events posted in - /// each batch. - /// - public const int BatchPostingLimit = 50; + /// + /// A reasonable default for the number of events posted in + /// each batch. + /// + public const int BatchPostingLimit = 50; - /// - /// The default name for the log collection. - /// - public const string CollectionName = "log"; + /// + /// The default name for the log collection. + /// + public const string CollectionName = "log"; - /// - /// Default capped collection max size in megabytes - /// - public const int CappedCollectionMaxSizeMb = 50; + /// + /// Default capped collection max size in megabytes + /// + public const int CappedCollectionMaxSizeMb = 50; - /// - /// A reasonable default time to wait between checking for event batches. - /// - public static readonly TimeSpan BatchPeriod = TimeSpan.FromSeconds(2); - } + /// + /// A reasonable default time to wait between checking for event batches. + /// + public static readonly TimeSpan BatchPeriod = TimeSpan.FromSeconds(2); } \ No newline at end of file diff --git a/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSinkLegacy.cs b/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSinkLegacy.cs index 6ecc378..b76c55e 100644 --- a/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSinkLegacy.cs +++ b/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/MongoDBSinkLegacy.cs @@ -24,93 +24,91 @@ using Serilog.Formatting; using Serilog.Helpers; -namespace Serilog.Sinks.MongoDB +namespace Serilog.Sinks.MongoDB; + +public class MongoDBSinkLegacy : MongoDBSinkBase { - public class MongoDBSinkLegacy : MongoDBSinkBase + private readonly ITextFormatter _formatter; + + public MongoDBSinkLegacy( + MongoDBSinkConfiguration configuration, + IFormatProvider? formatProvider = null, + ITextFormatter? textFormatter = null) + : base(configuration) { - private readonly ITextFormatter _formatter; + this._formatter = textFormatter ?? new MongoDBJsonFormatter( + renderMessage: true, + formatProvider: formatProvider); + } - public MongoDBSinkLegacy( - MongoDBSinkConfiguration configuration, - IFormatProvider? formatProvider = null, - ITextFormatter? textFormatter = null) - : base(configuration) - { - this._formatter = textFormatter ?? new MongoDBJsonFormatter( - renderMessage: true, - formatProvider: formatProvider); - } + /// + /// Creates Json from the log events enumerable + /// + /// + /// + private string GenerateJson(IEnumerable events) + { + var logEventLines = new List(); - /// - /// Creates Json from the log events enumerable - /// - /// - /// - private string GenerateJson(IEnumerable events) + foreach (var logEvent in events) { - var logEventLines = new List(); + string logEventLine; - foreach (var logEvent in events) + using (var writer = new StringWriter()) { - string logEventLine; - - using (var writer = new StringWriter()) - { - this._formatter.Format(logEvent, writer); - logEventLine = writer.ToString().Trim(); - } - - if (logEventLine.EndsWith("}")) - { - // remove so we can add Utc to end - logEventLine = logEventLine.Substring(0, logEventLine.Length - 1); - } + this._formatter.Format(logEvent, writer); + logEventLine = writer.ToString().Trim(); + } - logEventLine += $@", ""UtcTimestamp"": ""{logEvent.Timestamp.ToUniversalTime().DateTime:u}"" }}"; + if (logEventLine.EndsWith("}")) + // remove so we can add Utc to end + logEventLine = logEventLine.Substring(0, logEventLine.Length - 1); - logEventLines.Add(logEventLine); - } + logEventLine += + $@", ""UtcTimestamp"": ""{logEvent.Timestamp.ToUniversalTime().DateTime:u}"" }}"; - return $@"{{ ""logEvents"": [{string.Join(",", logEventLines)}] }}"; + logEventLines.Add(logEventLine); } - /// - /// Generate BSON documents from LogEvents. - /// - /// The events. - /// - /// - /// - protected IReadOnlyCollection GenerateBsonDocuments( - IEnumerable events) - { - if (events == null) throw new ArgumentNullException(nameof(events)); + return $@"{{ ""logEvents"": [{string.Join(",", logEventLines)}] }}"; + } - var json = this.GenerateJson(events); - var bson = BsonDocument.Parse(json); + /// + /// Generate BSON documents from LogEvents. + /// + /// The events. + /// + /// + /// + protected IReadOnlyCollection GenerateBsonDocuments( + IEnumerable events) + { + if (events == null) throw new ArgumentNullException(nameof(events)); - return bson["logEvents"].AsBsonArray - .Select(x => x.AsBsonDocument.SanitizeDocumentRecursive()).ToList(); - } + var json = this.GenerateJson(events); + var bson = BsonDocument.Parse(json); - /// - /// Emit a batch of log events, running asynchronously. - /// - /// The events to emit. - /// - /// - /// Override either - /// - /// or - /// - /// , - /// not both. Overriding EmitBatch() is preferred. - /// - protected override Task EmitBatchAsync(IEnumerable events) - { - return this.InsertMany(this.GenerateBsonDocuments(events)); - } + return bson["logEvents"].AsBsonArray + .Select(x => x.AsBsonDocument.SanitizeDocumentRecursive()).ToList(); + } + + /// + /// Emit a batch of log events, running asynchronously. + /// + /// The events to emit. + /// + /// + /// Override either + /// + /// or + /// + /// , + /// not both. Overriding EmitBatch() is preferred. + /// + protected override Task EmitBatchAsync(IEnumerable events) + { + return this.InsertMany(this.GenerateBsonDocuments(events)); } } \ No newline at end of file diff --git a/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/RollingInterval.cs b/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/RollingInterval.cs index 153fadb..7c1422f 100644 --- a/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/RollingInterval.cs +++ b/src/Serilog.Sinks.MongoDB/Sinks/MongoDB/RollingInterval.cs @@ -12,41 +12,40 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Serilog.Sinks.MongoDB +namespace Serilog.Sinks.MongoDB; + +/// +/// Specifies the frequency at which the log file should roll. +/// +public enum RollingInterval { /// - /// Specifies the frequency at which the log file should roll. + /// The log file will never roll; no time period information will be appended to the log file name. /// - public enum RollingInterval - { - /// - /// The log file will never roll; no time period information will be appended to the log file name. - /// - Infinite, + Infinite, - /// - /// Roll every year. Filenames will have a four-digit year appended in the pattern yyyy. - /// - Year, + /// + /// Roll every year. Filenames will have a four-digit year appended in the pattern yyyy. + /// + Year, - /// - /// Roll every calendar month. Filenames will have yyyyMM appended. - /// - Month, + /// + /// Roll every calendar month. Filenames will have yyyyMM appended. + /// + Month, - /// - /// Roll every day. Filenames will have yyyyMMdd appended. - /// - Day, + /// + /// Roll every day. Filenames will have yyyyMMdd appended. + /// + Day, - /// - /// Roll every hour. Filenames will have yyyyMMddHH appended. - /// - Hour, + /// + /// Roll every hour. Filenames will have yyyyMMddHH appended. + /// + Hour, - /// - /// Roll every minute. Filenames will have yyyyMMddHHmm appended. - /// - Minute - } + /// + /// Roll every minute. Filenames will have yyyyMMddHHmm appended. + /// + Minute } \ No newline at end of file diff --git a/test/Serilog.Sinks.MongoDB.Tests/LoggerConfigurationMongoDBExtensionsTests.cs b/test/Serilog.Sinks.MongoDB.Tests/LoggerConfigurationMongoDBExtensionsTests.cs index 1c9ac73..e1558e4 100644 --- a/test/Serilog.Sinks.MongoDB.Tests/LoggerConfigurationMongoDBExtensionsTests.cs +++ b/test/Serilog.Sinks.MongoDB.Tests/LoggerConfigurationMongoDBExtensionsTests.cs @@ -1,6 +1,4 @@ -using System; - -using FluentAssertions; +using FluentAssertions; using Microsoft.Extensions.Configuration; @@ -39,7 +37,6 @@ private static void TestCollectionAndDocumentExists(RollingInterval? rollingInte configuration.SetCollectionName(MongoCollectionName); }).CreateLogger()) { - logger.Information(Message); } diff --git a/test/Serilog.Sinks.MongoDB.Tests/Serilog.Sinks.MongoDB.Tests.csproj b/test/Serilog.Sinks.MongoDB.Tests/Serilog.Sinks.MongoDB.Tests.csproj index 89a8006..02b2f3b 100644 --- a/test/Serilog.Sinks.MongoDB.Tests/Serilog.Sinks.MongoDB.Tests.csproj +++ b/test/Serilog.Sinks.MongoDB.Tests/Serilog.Sinks.MongoDB.Tests.csproj @@ -2,6 +2,7 @@ net6.0 + latest