• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

loresoft / NLog.Mongo / 11217039547

07 Oct 2024 01:52PM UTC coverage: 61.67%. Remained the same
11217039547

push

github

web-flow
Create FUNDING.yml

125 of 230 branches covered (54.35%)

Branch coverage included in aggregate %.

200 of 297 relevant lines covered (67.34%)

21.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.64
/src/NLog.Mongo/MongoTarget.cs
1
using System;
2
using System.Collections.Concurrent;
3
using System.Collections.Generic;
4
using System.Globalization;
5
using System.Linq;
6
using System.Reflection;
7
using System.Runtime.InteropServices;
8
using MongoDB.Bson;
9
using MongoDB.Driver;
10
using NLog.Common;
11
using NLog.Config;
12
using NLog.Layouts;
13
using NLog.Targets;
14

15
namespace NLog.Mongo
16
{
17
    /// <summary>
18
    /// NLog message target for MongoDB.
19
    /// </summary>
20
    [Target("Mongo")]
21
    public class MongoTarget : Target
22
    {
23
        private struct MongoConnectionKey : IEquatable<MongoConnectionKey>
24
        {
25
            private readonly string ConnectionString;
26
            private readonly string CollectionName;
27
            private readonly string DatabaseName;
28

29
            public MongoConnectionKey(string connectionString, string collectionName, string databaseName)
30
            {
31
                ConnectionString = connectionString ?? string.Empty;
32!
32
                CollectionName = collectionName ?? string.Empty;
32!
33
                DatabaseName = databaseName ?? string.Empty;
32!
34
            }
32✔
35

36
            public bool Equals(MongoConnectionKey other)
37
            {
38
                return ConnectionString == other.ConnectionString
29!
39
                    && CollectionName == other.CollectionName
29✔
40
                    && DatabaseName == other.DatabaseName;
29✔
41
            }
42

43
            public override int GetHashCode()
44
            {
45
                return ConnectionString.GetHashCode() ^ CollectionName.GetHashCode() ^ DatabaseName.GetHashCode();
35✔
46
            }
47

48
            public override bool Equals(object obj)
49
            {
50
                return obj is MongoConnectionKey && Equals((MongoConnectionKey)obj);
×
51
            }
52
        }
53

54
        private static readonly ConcurrentDictionary<MongoConnectionKey, IMongoCollection<BsonDocument>> _collectionCache = new ConcurrentDictionary<MongoConnectionKey, IMongoCollection<BsonDocument>>();
1✔
55
        private Func<AsyncLogEventInfo, BsonDocument> _createDocumentDelegate;
56
        private static readonly LogEventInfo _defaultLogEvent = NLog.LogEventInfo.CreateNullEvent();
1✔
57

58
        /// <summary>
59
        /// Initializes a new instance of the <see cref="MongoTarget"/> class.
60
        /// </summary>
61
        public MongoTarget()
4✔
62
        {
63
            Fields = new List<MongoField>();
4✔
64
            Properties = new List<MongoField>();
4✔
65
            IncludeDefaults = true;
4✔
66
            IncludeEventProperties = true;
4✔
67
        }
4✔
68

69
        /// <summary>
70
        /// Gets the fields collection.
71
        /// </summary>
72
        /// <value>
73
        /// The fields.
74
        /// </value>
75
        [ArrayParameter(typeof(MongoField), "field")]
76
        public IList<MongoField> Fields { get; private set; }
360✔
77

78
        /// <summary>
79
        /// Gets the properties collection.
80
        /// </summary>
81
        /// <value>
82
        /// The properties.
83
        /// </value>
84
        [ArrayParameter(typeof(MongoField), "property")]
85
        public IList<MongoField> Properties { get; private set; }
354✔
86

87
        /// <summary>
88
        /// Gets or sets the connection string name string.
89
        /// </summary>
90
        /// <value>
91
        /// The connection name string.
92
        /// </value>
93
        public string ConnectionString
94
        {
95
            get => (_connectionString as SimpleLayout)?.Text;
×
96
            set => _connectionString = value ?? string.Empty;
4!
97
        }
98
        private Layout _connectionString;
99

100
        /// <summary>
101
        /// Gets or sets the name of the connection.
102
        /// </summary>
103
        /// <value>
104
        /// The name of the connection.
105
        /// </value>
106
        public string ConnectionName { get; set; }
4✔
107

108
        /// <summary>
109
        /// Gets or sets a value indicating whether to use the default document format.
110
        /// </summary>
111
        /// <value>
112
        ///   <c>true</c> to use the default document format; otherwise, <c>false</c>.
113
        /// </value>
114
        public bool IncludeDefaults { get; set; }
41✔
115

116
        /// <summary>
117
        /// Gets or sets the name of the database.
118
        /// </summary>
119
        /// <value>
120
        /// The name of the database.
121
        /// </value>
122
        public string DatabaseName
123
        {
124
            get => (_databaseName as SimpleLayout)?.Text;
×
125
            set => _databaseName = value ?? string.Empty;
1!
126
        }
127
        private Layout _databaseName;
128

129
        /// <summary>
130
        /// Gets or sets the name of the collection.
131
        /// </summary>
132
        /// <value>
133
        /// The name of the collection.
134
        /// </value>
135
        public string CollectionName
136
        {
137
            get => (_collectionName as SimpleLayout)?.Text;
×
138
            set => _collectionName = value ?? string.Empty;
4!
139
        }
140
        private Layout _collectionName;
141

142
        /// <summary>
143
        /// Gets or sets the size in bytes of the capped collection.
144
        /// </summary>
145
        /// <value>
146
        /// The size of the capped collection.
147
        /// </value>
148
        public long? CappedCollectionSize { get; set; }
10✔
149

150
        /// <summary>
151
        /// Gets or sets the capped collection max items.
152
        /// </summary>
153
        /// <value>
154
        /// The capped collection max items.
155
        /// </value>
156
        public long? CappedCollectionMaxItems { get; set; }
3✔
157

158
        /// <summary>
159
        /// Gets or sets a value indicating whether to include per-event properties in the payload sent to MongoDB
160
        /// </summary>
161
        public bool IncludeEventProperties { get; set; }
61✔
162

163
        /// <summary>
164
        /// Initializes the target. Can be used by inheriting classes
165
        /// to initialize logging.
166
        /// </summary>
167
        /// <exception cref="NLog.NLogConfigurationException">Can not resolve MongoDB ConnectionString. Please make sure the ConnectionString property is set.</exception>
168
        protected override void InitializeTarget()
169
        {
170
            base.InitializeTarget();
4✔
171

172
            if (!string.IsNullOrEmpty(ConnectionName))
4!
173
                ConnectionString = GetConnectionString(ConnectionName);
×
174

175
            var connectionString = _connectionString?.Render(_defaultLogEvent);
4!
176
            if (string.IsNullOrEmpty(connectionString))
4!
177
                throw new NLogConfigurationException("Can not resolve MongoDB ConnectionString. Please make sure the ConnectionString property is set.");
×
178
        }
4✔
179

180
        /// <summary>
181
        /// Writes an array of logging events to the log target. By default it iterates on all
182
        /// events and passes them to "Write" method. Inheriting classes can use this method to
183
        /// optimize batch writes.
184
        /// </summary>
185
        /// <param name="logEvents">Logging events to be written out.</param>
186
        protected override void Write(IList<AsyncLogEventInfo> logEvents)
187
        {
188
            if (logEvents.Count == 0)
14!
189
                return;
×
190

191
            try
192
            {
193
                if (_createDocumentDelegate == null)
14✔
194
                    _createDocumentDelegate = e => CreateDocument(e.LogEvent);
20✔
195

196
                var documents = logEvents.Select(_createDocumentDelegate);
14✔
197
                var collection = GetCollection(logEvents[logEvents.Count - 1].LogEvent.TimeStamp);
14✔
198
                collection.InsertMany(documents);
14✔
199

200
                for (int i = 0; i < logEvents.Count; ++i)
64✔
201
                    logEvents[i].Continuation(null);
18✔
202
            }
14✔
203
            catch (Exception ex)
×
204
            {
205
                InternalLogger.Error("Error when writing to MongoDB {0}", ex);
×
206

207
                if (ex.MustBeRethrownImmediately())
×
208
                    throw;
×
209

210
                for (int i = 0; i < logEvents.Count; ++i)
×
211
                    logEvents[i].Continuation(ex);
×
212

213
                if (ex.MustBeRethrown())
×
214
                    throw;
×
215
            }
×
216
        }
14✔
217

218
        /// <summary>
219
        /// Writes logging event to the log target.
220
        /// classes.
221
        /// </summary>
222
        /// <param name="logEvent">Logging event to be written out.</param>
223
        protected override void Write(LogEventInfo logEvent)
224
        {
225
            try
226
            {
227
                var document = CreateDocument(logEvent);
18✔
228
                var collection = GetCollection(logEvent.TimeStamp);
18✔
229
                collection.InsertOne(document);
18✔
230
            }
18✔
231
            catch (Exception ex)
×
232
            {
233
                InternalLogger.Error("Error when writing to MongoDB {0}", ex);
×
234
                throw;
×
235
            }
236
        }
18✔
237

238
        private BsonDocument CreateDocument(LogEventInfo logEvent)
239
        {
240
            var document = new BsonDocument();
36✔
241
            if (IncludeDefaults || Fields.Count == 0)
36✔
242
                AddDefaults(document, logEvent);
27✔
243

244
            // extra fields
245
            for (int i = 0; i < Fields.Count; ++i)
270✔
246
            {
247
                var value = GetValue(Fields[i], logEvent);
99✔
248
                if (value != null)
99✔
249
                    document[Fields[i].Name] = value;
82✔
250
            }
251

252
            AddProperties(document, logEvent);
36✔
253

254
            return document;
36✔
255
        }
256

257
        private void AddDefaults(BsonDocument document, LogEventInfo logEvent)
258
        {
259
            document.Add("Date", new BsonDateTime(logEvent.TimeStamp));
27✔
260

261
            if (logEvent.Level != null)
27✔
262
                document.Add("Level", new BsonString(logEvent.Level.Name));
27✔
263

264
            if (logEvent.LoggerName != null)
27✔
265
                document.Add("Logger", new BsonString(logEvent.LoggerName));
27✔
266

267
            if (logEvent.FormattedMessage != null)
27✔
268
                document.Add("Message", new BsonString(logEvent.FormattedMessage));
27✔
269

270
            if (logEvent.Exception != null)
27✔
271
                document.Add("Exception", CreateException(logEvent.Exception));
3✔
272
        }
27✔
273

274
        private void AddProperties(BsonDocument document, LogEventInfo logEvent)
275
        {
276
            if ((IncludeEventProperties && logEvent.HasProperties) || Properties.Count > 0)
36✔
277
            {
278
                var propertiesDocument = new BsonDocument();
20✔
279
                for (int i = 0; i < Properties.Count; ++i)
220✔
280
                {
281
                    string key = Properties[i].Name;
90✔
282
                    var value = GetValue(Properties[i], logEvent);
90✔
283

284
                    if (value != null)
90✔
285
                        propertiesDocument[key] = value;
72✔
286
                }
287

288
                if (IncludeEventProperties && logEvent.HasProperties)
20✔
289
                {
290
                    foreach (var property in logEvent.Properties)
24✔
291
                    {
292
                        if (property.Key == null || property.Value == null)
6✔
293
                            continue;
294

295
                        string key = Convert.ToString(property.Key, CultureInfo.InvariantCulture);
6✔
296
                        if (string.IsNullOrEmpty(key))
6✔
297
                            continue;
298

299
                        string value = Convert.ToString(property.Value, CultureInfo.InvariantCulture);
6✔
300
                        if (string.IsNullOrEmpty(value))
6✔
301
                            continue;
302

303
                        if (key.IndexOf('.') >= 0)
6!
304
                            key = key.Replace('.', '_');
×
305

306
                        propertiesDocument[key] = new BsonString(value);
6✔
307
                    }
308
                }
309

310
                if (propertiesDocument.ElementCount > 0)
20✔
311
                    document.Add("Properties", propertiesDocument);
20✔
312
            }
313
        }
36✔
314

315
        private BsonValue CreateException(Exception exception)
316
        {
317
            if (exception == null)
3!
318
                return BsonNull.Value;
×
319

320
            if (exception is AggregateException aggregateException)
3!
321
            {
322
                aggregateException = aggregateException.Flatten();
×
323
                if (aggregateException.InnerExceptions?.Count == 1)
×
324
                {
325
                    exception = aggregateException.InnerExceptions[0];
×
326
                }
327
                else
328
                {
329
                    exception = aggregateException;
×
330
                }
331
            }
332

333
            var document = new BsonDocument();
3✔
334
            document.Add("Message", new BsonString(exception.Message));
3✔
335
            document.Add("BaseMessage", new BsonString(exception.GetBaseException().Message));
3✔
336
            document.Add("Text", new BsonString(exception.ToString()));
3✔
337
            document.Add("Type", new BsonString(exception.GetType().ToString()));
3✔
338

339
#if !NETSTANDARD1_5
340
            if (exception is ExternalException external)
3!
341
                document.Add("ErrorCode", new BsonInt32(external.ErrorCode));
×
342
#endif
343
            document.Add("HResult", new BsonInt32(exception.HResult));
3✔
344
            document.Add("Source", new BsonString(exception.Source ?? string.Empty));
3!
345

346
#if !NETSTANDARD1_5
347
            var method = exception.TargetSite;
3✔
348
            if (method != null)
3✔
349
            {
350
                document.Add("MethodName", new BsonString(method.Name ?? string.Empty));
3!
351

352
                AssemblyName assembly = method.Module?.Assembly?.GetName();
3!
353
                if (assembly != null)
3✔
354
                {
355
                    document.Add("ModuleName", new BsonString(assembly.Name));
3✔
356
                    document.Add("ModuleVersion", new BsonString(assembly.Version.ToString()));
3✔
357
                }
358
            }
359
#endif
360

361
            return document;
3✔
362
        }
363

364

365
        private BsonValue GetValue(MongoField field, LogEventInfo logEvent)
366
        {
367
            var value = (field.Layout != null ? RenderLogEvent(field.Layout, logEvent) : string.Empty).Trim();
189!
368
            if (string.IsNullOrEmpty(value))
189✔
369
                return null;
35✔
370

371
            BsonValue bsonValue = null;
154✔
372
            switch (field.BsonTypeCode)
154!
373
            {
374
                case TypeCode.Boolean:
375
                    MongoConvert.TryBoolean(value, out bsonValue);
×
376
                    break;
×
377
                case TypeCode.DateTime:
378
                    MongoConvert.TryDateTime(value, out bsonValue);
9✔
379
                    break;
9✔
380
                case TypeCode.Double:
381
                    MongoConvert.TryDouble(value, out bsonValue);
×
382
                    break;
×
383
                case TypeCode.Int32:
384
                    MongoConvert.TryInt32(value, out bsonValue);
54✔
385
                    break;
54✔
386
                case TypeCode.Int64:
387
                    MongoConvert.TryInt64(value, out bsonValue);
×
388
                    break;
×
389
                case TypeCode.Object:
390
                    MongoConvert.TryJsonObject(value, out bsonValue);
9✔
391
                    break;
392
            }
393

394
            return bsonValue ?? new BsonString(value);
154✔
395
        }
396

397
        private IMongoCollection<BsonDocument> GetCollection(DateTime timestamp)
398
        {
399
            if (_defaultLogEvent.TimeStamp < timestamp)
32✔
400
                _defaultLogEvent.TimeStamp = timestamp;
11✔
401

402
            string connectionString = _connectionString != null ? RenderLogEvent(_connectionString, _defaultLogEvent) : string.Empty;
32!
403
            string collectionName = _collectionName != null ? RenderLogEvent(_collectionName, _defaultLogEvent) : string.Empty;
32!
404
            string databaseName = _databaseName != null ? RenderLogEvent(_databaseName, _defaultLogEvent) : string.Empty;
32✔
405

406
            if (string.IsNullOrEmpty(connectionString))
32!
407
                throw new NLogConfigurationException("Can not resolve MongoDB ConnectionString. Please make sure the ConnectionString property is set.");
×
408

409
            // cache mongo collection based on target name.
410
            var key = new MongoConnectionKey(connectionString, collectionName, databaseName);
32✔
411
            if (_collectionCache.TryGetValue(key, out var mongoCollection))
32✔
412
                return mongoCollection;
29✔
413

414
            return _collectionCache.GetOrAdd(key, k =>
3✔
415
            {
3✔
416
                // create collection
3✔
417
                var mongoUrl = new MongoUrl(connectionString);
3✔
418

3✔
419
                databaseName = !string.IsNullOrEmpty(databaseName) ? databaseName : (mongoUrl.DatabaseName ?? "NLog");
3!
420
                collectionName = !string.IsNullOrEmpty(collectionName) ? collectionName : "Log";
3!
421
                InternalLogger.Info("Connecting to MongoDB collection {0} in database {1}", collectionName, databaseName);
3✔
422

3✔
423
                var client = new MongoClient(mongoUrl);
3✔
424

3✔
425
                // Database name overrides connection string
3✔
426
                var database = client.GetDatabase(databaseName);
3✔
427

3✔
428
                if (CappedCollectionSize.HasValue)
3✔
429
                {
3✔
430
                    InternalLogger.Debug("Checking for existing MongoDB collection {0} in database {1}", collectionName, databaseName);
3✔
431
                    
3✔
432
                    var filterOptions = new ListCollectionNamesOptions { Filter = new BsonDocument("name", collectionName) };
3✔
433
                    if (!database.ListCollectionNames(filterOptions).Any())
3✔
434
                    {
3✔
435
                        InternalLogger.Debug("Creating new MongoDB collection {0} in database {1}", collectionName, databaseName);
3✔
436

3✔
437
                        // create capped
3✔
438
                        var options = new CreateCollectionOptions
3✔
439
                        {
3✔
440
                            Capped = true,
3✔
441
                            MaxSize = CappedCollectionSize,
3✔
442
                            MaxDocuments = CappedCollectionMaxItems
3✔
443
                        };
3✔
444

3✔
445
                        database.CreateCollection(collectionName, options);
3✔
446
                    }
3✔
447
                }
3✔
448

3✔
449
                var collection = database.GetCollection<BsonDocument>(collectionName);
3✔
450
                InternalLogger.Debug("Retrieved MongoDB collection {0} from database {1}", collectionName, databaseName);
3✔
451
                return collection;
3✔
452
            });
3✔
453
        }
454

455
        private static string GetConnectionString(string connectionName)
456
        {
457
            if (connectionName == null)
×
458
                throw new ArgumentNullException(nameof(connectionName));
×
459

460
#if NETSTANDARD1_5 || NETSTANDARD2_0
461
            return null;
×
462
#else
463
            var settings = System.Configuration.ConfigurationManager.ConnectionStrings[connectionName];
464
            if (settings == null)
465
                throw new NLogConfigurationException($"No connection string named '{connectionName}' could be found in the application configuration file.");
466

467
            string connectionString = settings.ConnectionString;
468
            if (string.IsNullOrEmpty(connectionString))
469
                throw new NLogConfigurationException($"The connection string '{connectionName}' in the application's configuration file does not contain the required connectionString attribute.");
470

471
            return connectionString;
472
#endif
473
        }
474
    }
475
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc