LogManager.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797
  1. /*
  2. Technitium DNS Server
  3. Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com)
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. using Microsoft.AspNetCore.Http;
  16. using System;
  17. using System.Collections.Concurrent;
  18. using System.Globalization;
  19. using System.IO;
  20. using System.IO.Compression;
  21. using System.Linq;
  22. using System.Net;
  23. using System.Text;
  24. using System.Threading;
  25. using System.Threading.Tasks;
  26. using TechnitiumLibrary.IO;
  27. using TechnitiumLibrary.Net.Dns;
  28. using TechnitiumLibrary.Net.Dns.EDnsOptions;
  29. using TechnitiumLibrary.Net.Dns.ResourceRecords;
  30. namespace DnsServerCore
  31. {
  32. public sealed class LogManager : IDisposable
  33. {
  34. #region variables
  35. readonly string _configFolder;
  36. bool _enableLogging;
  37. string _logFolder;
  38. int _maxLogFileDays;
  39. bool _useLocalTime;
  40. const string LOG_ENTRY_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
  41. const string LOG_FILE_DATE_TIME_FORMAT = "yyyy-MM-dd";
  42. string _logFile;
  43. StreamWriter _logOut;
  44. DateTime _logDate;
  45. readonly BlockingCollection<LogQueueItem> _queue = new BlockingCollection<LogQueueItem>();
  46. Thread _consumerThread;
  47. readonly object _logFileLock = new object();
  48. readonly object _queueLock = new object();
  49. readonly EventWaitHandle _queueWait = new AutoResetEvent(false);
  50. CancellationTokenSource _queueCancellationTokenSource = new CancellationTokenSource();
  51. readonly Timer _logCleanupTimer;
  52. const int LOG_CLEANUP_TIMER_INITIAL_INTERVAL = 60 * 1000;
  53. const int LOG_CLEANUP_TIMER_PERIODIC_INTERVAL = 60 * 60 * 1000;
  54. #endregion
  55. #region constructor
  56. public LogManager(string configFolder)
  57. {
  58. _configFolder = configFolder;
  59. AppDomain.CurrentDomain.UnhandledException += delegate (object sender, UnhandledExceptionEventArgs e)
  60. {
  61. if (!_enableLogging)
  62. {
  63. Console.WriteLine(e.ExceptionObject.ToString());
  64. return;
  65. }
  66. lock (_queueLock)
  67. {
  68. try
  69. {
  70. _queueCancellationTokenSource.Cancel();
  71. lock (_logFileLock)
  72. {
  73. if (_logOut != null)
  74. WriteLog(DateTime.UtcNow, e.ExceptionObject.ToString());
  75. }
  76. }
  77. catch (ObjectDisposedException)
  78. { }
  79. catch (Exception ex)
  80. {
  81. Console.WriteLine(e.ExceptionObject.ToString());
  82. Console.WriteLine(ex.ToString());
  83. }
  84. finally
  85. {
  86. _queueWait.Set();
  87. }
  88. }
  89. };
  90. _logCleanupTimer = new Timer(delegate (object state)
  91. {
  92. try
  93. {
  94. if (_maxLogFileDays < 1)
  95. return;
  96. DateTime cutoffDate = DateTime.UtcNow.AddDays(_maxLogFileDays * -1).Date;
  97. DateTimeStyles dateTimeStyles;
  98. if (_useLocalTime)
  99. dateTimeStyles = DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal;
  100. else
  101. dateTimeStyles = DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal;
  102. foreach (string logFile in ListLogFiles())
  103. {
  104. string logFileName = Path.GetFileNameWithoutExtension(logFile);
  105. if (!DateTime.TryParseExact(logFileName, LOG_FILE_DATE_TIME_FORMAT, CultureInfo.InvariantCulture, dateTimeStyles, out DateTime logFileDate))
  106. continue;
  107. if (logFileDate < cutoffDate)
  108. {
  109. try
  110. {
  111. File.Delete(logFile);
  112. Write("LogManager cleanup deleted the log file: " + logFile);
  113. }
  114. catch (Exception ex)
  115. {
  116. Write(ex);
  117. }
  118. }
  119. }
  120. }
  121. catch (Exception ex)
  122. {
  123. Write(ex);
  124. }
  125. });
  126. LoadConfig();
  127. if (_enableLogging)
  128. StartLogging();
  129. }
  130. #endregion
  131. #region IDisposable
  132. bool _disposed;
  133. private void Dispose(bool disposing)
  134. {
  135. lock (_queueLock)
  136. {
  137. try
  138. {
  139. _queueCancellationTokenSource.Cancel();
  140. lock (_logFileLock)
  141. {
  142. if (_disposed)
  143. return;
  144. if (disposing)
  145. {
  146. if (_logOut != null)
  147. {
  148. WriteLog(DateTime.UtcNow, "Logging stopped.");
  149. _logOut.Dispose();
  150. }
  151. _logCleanupTimer.Dispose();
  152. }
  153. _disposed = true;
  154. }
  155. }
  156. finally
  157. {
  158. _queueWait.Set();
  159. }
  160. }
  161. }
  162. public void Dispose()
  163. {
  164. Dispose(true);
  165. }
  166. #endregion
  167. #region private
  168. internal void StartLogging()
  169. {
  170. StartNewLog();
  171. _queueWait.Set();
  172. //start consumer thread
  173. _consumerThread = new Thread(delegate ()
  174. {
  175. while (true)
  176. {
  177. _queueWait.WaitOne();
  178. Monitor.Enter(_logFileLock);
  179. try
  180. {
  181. if (_disposed || (_logOut == null))
  182. break;
  183. foreach (LogQueueItem item in _queue.GetConsumingEnumerable(_queueCancellationTokenSource.Token))
  184. {
  185. if (_useLocalTime)
  186. {
  187. DateTime messageLocalDateTime = item._dateTime.ToLocalTime();
  188. if (messageLocalDateTime.Date > _logDate)
  189. {
  190. WriteLog(DateTime.UtcNow, "Logging stopped.");
  191. StartNewLog();
  192. }
  193. WriteLog(messageLocalDateTime, item._message);
  194. }
  195. else
  196. {
  197. if (item._dateTime.Date > _logDate)
  198. {
  199. WriteLog(DateTime.UtcNow, "Logging stopped.");
  200. StartNewLog();
  201. }
  202. WriteLog(item._dateTime, item._message);
  203. }
  204. }
  205. }
  206. catch (ObjectDisposedException)
  207. { }
  208. catch (OperationCanceledException)
  209. { }
  210. finally
  211. {
  212. Monitor.Exit(_logFileLock);
  213. }
  214. _queueCancellationTokenSource = new CancellationTokenSource();
  215. }
  216. });
  217. _consumerThread.Name = "Log";
  218. _consumerThread.IsBackground = true;
  219. _consumerThread.Start();
  220. }
  221. internal void StopLogging()
  222. {
  223. lock (_queueLock)
  224. {
  225. try
  226. {
  227. if (_logOut != null)
  228. _queueCancellationTokenSource.Cancel();
  229. lock (_logFileLock)
  230. {
  231. if (_logOut != null)
  232. {
  233. WriteLog(DateTime.UtcNow, "Logging stopped.");
  234. _logOut.Dispose();
  235. _logOut = null; //to stop consumer thread
  236. }
  237. }
  238. }
  239. finally
  240. {
  241. _queueWait.Set();
  242. }
  243. }
  244. }
  245. internal void LoadConfig()
  246. {
  247. string logConfigFile = Path.Combine(_configFolder, "log.config");
  248. try
  249. {
  250. using (FileStream fS = new FileStream(logConfigFile, FileMode.Open, FileAccess.Read))
  251. {
  252. BinaryReader bR = new BinaryReader(fS);
  253. if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "LS") //format
  254. throw new InvalidDataException("DnsServer log config file format is invalid.");
  255. byte version = bR.ReadByte();
  256. switch (version)
  257. {
  258. case 1:
  259. _enableLogging = bR.ReadBoolean();
  260. _logFolder = bR.ReadShortString();
  261. _maxLogFileDays = bR.ReadInt32();
  262. _useLocalTime = bR.ReadBoolean();
  263. break;
  264. default:
  265. throw new InvalidDataException("DnsServer log config version not supported.");
  266. }
  267. }
  268. }
  269. catch (FileNotFoundException)
  270. {
  271. _enableLogging = true;
  272. _logFolder = "logs";
  273. _maxLogFileDays = 0;
  274. _useLocalTime = false;
  275. SaveConfig();
  276. }
  277. catch (Exception ex)
  278. {
  279. Console.Write(ex.ToString());
  280. SaveConfig();
  281. }
  282. if (_maxLogFileDays == 0)
  283. _logCleanupTimer.Change(Timeout.Infinite, Timeout.Infinite);
  284. else
  285. _logCleanupTimer.Change(LOG_CLEANUP_TIMER_INITIAL_INTERVAL, LOG_CLEANUP_TIMER_PERIODIC_INTERVAL);
  286. }
  287. private string ConvertToRelativePath(string path)
  288. {
  289. if (path.StartsWith(_configFolder, StringComparison.OrdinalIgnoreCase))
  290. path = path.Substring(_configFolder.Length).TrimStart(Path.DirectorySeparatorChar);
  291. return path;
  292. }
  293. private string ConvertToAbsolutePath(string path)
  294. {
  295. if (Path.IsPathRooted(path))
  296. return path;
  297. return Path.Combine(_configFolder, path);
  298. }
  299. private void SaveConfig()
  300. {
  301. string logConfigFile = Path.Combine(_configFolder, "log.config");
  302. using (MemoryStream mS = new MemoryStream())
  303. {
  304. //serialize config
  305. BinaryWriter bW = new BinaryWriter(mS);
  306. bW.Write(Encoding.ASCII.GetBytes("LS")); //format
  307. bW.Write((byte)1); //version
  308. bW.Write(_enableLogging);
  309. bW.WriteShortString(_logFolder);
  310. bW.Write(_maxLogFileDays);
  311. bW.Write(_useLocalTime);
  312. //write config
  313. mS.Position = 0;
  314. using (FileStream fS = new FileStream(logConfigFile, FileMode.Create, FileAccess.Write))
  315. {
  316. mS.CopyTo(fS);
  317. }
  318. }
  319. }
  320. private void StartNewLog()
  321. {
  322. if (_logOut != null)
  323. _logOut.Dispose();
  324. string logFolder = ConvertToAbsolutePath(_logFolder);
  325. if (!Directory.Exists(logFolder))
  326. Directory.CreateDirectory(logFolder);
  327. DateTime logStartDateTime;
  328. if (_useLocalTime)
  329. logStartDateTime = DateTime.Now;
  330. else
  331. logStartDateTime = DateTime.UtcNow;
  332. _logFile = Path.Combine(logFolder, logStartDateTime.ToString(LOG_FILE_DATE_TIME_FORMAT) + ".log");
  333. _logOut = new StreamWriter(new FileStream(_logFile, FileMode.Append, FileAccess.Write, FileShare.Read));
  334. _logDate = logStartDateTime.Date;
  335. WriteLog(logStartDateTime, "Logging started.");
  336. }
  337. private void WriteLog(DateTime dateTime, string message)
  338. {
  339. if (_useLocalTime)
  340. {
  341. if (dateTime.Kind == DateTimeKind.Local)
  342. _logOut.WriteLine("[" + dateTime.ToString(LOG_ENTRY_DATE_TIME_FORMAT) + " Local] " + message);
  343. else
  344. _logOut.WriteLine("[" + dateTime.ToLocalTime().ToString(LOG_ENTRY_DATE_TIME_FORMAT) + " Local] " + message);
  345. }
  346. else
  347. {
  348. if (dateTime.Kind == DateTimeKind.Utc)
  349. _logOut.WriteLine("[" + dateTime.ToString(LOG_ENTRY_DATE_TIME_FORMAT) + " UTC] " + message);
  350. else
  351. _logOut.WriteLine("[" + dateTime.ToUniversalTime().ToString(LOG_ENTRY_DATE_TIME_FORMAT) + " UTC] " + message);
  352. }
  353. _logOut.Flush();
  354. }
  355. #endregion
  356. #region public
  357. public string[] ListLogFiles()
  358. {
  359. return Directory.GetFiles(ConvertToAbsolutePath(_logFolder), "*.log", SearchOption.TopDirectoryOnly);
  360. }
  361. public async Task DownloadLogAsync(HttpContext context, string logName, long limit)
  362. {
  363. string logFileName = logName + ".log";
  364. using (FileStream fS = new FileStream(Path.Combine(ConvertToAbsolutePath(_logFolder), logFileName), FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 64 * 1024, true))
  365. {
  366. HttpResponse response = context.Response;
  367. response.ContentType = "text/plain";
  368. response.Headers.ContentDisposition = "attachment;filename=" + logFileName;
  369. if ((limit > fS.Length) || (limit < 1))
  370. limit = fS.Length;
  371. OffsetStream oFS = new OffsetStream(fS, 0, limit);
  372. HttpRequest request = context.Request;
  373. Stream s;
  374. string acceptEncoding = request.Headers["Accept-Encoding"];
  375. if (string.IsNullOrEmpty(acceptEncoding))
  376. {
  377. s = response.Body;
  378. }
  379. else
  380. {
  381. string[] acceptEncodingParts = acceptEncoding.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  382. if (acceptEncodingParts.Contains("br"))
  383. {
  384. response.Headers.ContentEncoding = "br";
  385. s = new BrotliStream(response.Body, CompressionMode.Compress);
  386. }
  387. else if (acceptEncodingParts.Contains("gzip"))
  388. {
  389. response.Headers.ContentEncoding = "gzip";
  390. s = new GZipStream(response.Body, CompressionMode.Compress);
  391. }
  392. else if (acceptEncodingParts.Contains("deflate"))
  393. {
  394. response.Headers.ContentEncoding = "deflate";
  395. s = new DeflateStream(response.Body, CompressionMode.Compress);
  396. }
  397. else
  398. {
  399. s = response.Body;
  400. }
  401. }
  402. await using (s)
  403. {
  404. await oFS.CopyToAsync(s);
  405. if (fS.Length > limit)
  406. await s.WriteAsync(Encoding.UTF8.GetBytes("\r\n####___TRUNCATED___####"));
  407. }
  408. }
  409. }
  410. public void DeleteLog(string logName)
  411. {
  412. string logFile = Path.Combine(ConvertToAbsolutePath(_logFolder), logName + ".log");
  413. if (logFile.Equals(_logFile, StringComparison.OrdinalIgnoreCase))
  414. DeleteCurrentLogFile();
  415. else
  416. File.Delete(logFile);
  417. }
  418. public void DeleteAllLogs()
  419. {
  420. string[] logFiles = ListLogFiles();
  421. foreach (string logFile in logFiles)
  422. {
  423. if (logFile.Equals(_logFile, StringComparison.OrdinalIgnoreCase))
  424. DeleteCurrentLogFile();
  425. else
  426. File.Delete(logFile);
  427. }
  428. }
  429. public void Write(Exception ex)
  430. {
  431. Write(ex.ToString());
  432. }
  433. public void Write(IPEndPoint ep, Exception ex)
  434. {
  435. Write(ep, ex.ToString());
  436. }
  437. public void Write(IPEndPoint ep, string message)
  438. {
  439. string ipInfo;
  440. if (ep == null)
  441. ipInfo = "";
  442. else if (ep.Address.IsIPv4MappedToIPv6)
  443. ipInfo = "[" + ep.Address.MapToIPv4().ToString() + ":" + ep.Port + "] ";
  444. else
  445. ipInfo = "[" + ep.ToString() + "] ";
  446. Write(ipInfo + message);
  447. }
  448. public void Write(IPEndPoint ep, DnsTransportProtocol protocol, Exception ex)
  449. {
  450. Write(ep, protocol, ex.ToString());
  451. }
  452. public void Write(IPEndPoint ep, DnsTransportProtocol protocol, DnsDatagram request, DnsDatagram response)
  453. {
  454. DnsQuestionRecord q = null;
  455. if (request.Question.Count > 0)
  456. q = request.Question[0];
  457. string question;
  458. if (q is null)
  459. question = "MISSING QUESTION!";
  460. else
  461. question = "QNAME: " + q.Name + "; QTYPE: " + q.Type.ToString() + "; QCLASS: " + q.Class;
  462. string responseInfo;
  463. if (response is null)
  464. {
  465. responseInfo = " NO RESPONSE FROM SERVER!";
  466. }
  467. else
  468. {
  469. string answer;
  470. if (response.Answer.Count == 0)
  471. {
  472. if (response.Truncation)
  473. answer = "[TRUNCATED]";
  474. else
  475. answer = "[]";
  476. }
  477. else if ((response.Answer.Count > 2) && response.IsZoneTransfer)
  478. {
  479. answer = "[ZONE TRANSFER]";
  480. }
  481. else
  482. {
  483. answer = "[";
  484. for (int i = 0; i < response.Answer.Count; i++)
  485. {
  486. if (i > 0)
  487. answer += ", ";
  488. answer += response.Answer[i].RDATA.ToString();
  489. }
  490. answer += "]";
  491. if (response.Additional.Count > 0)
  492. {
  493. switch (q.Type)
  494. {
  495. case DnsResourceRecordType.NS:
  496. case DnsResourceRecordType.MX:
  497. case DnsResourceRecordType.SRV:
  498. answer += "; ADDITIONAL: [";
  499. for (int i = 0; i < response.Additional.Count; i++)
  500. {
  501. DnsResourceRecord additional = response.Additional[i];
  502. switch (additional.Type)
  503. {
  504. case DnsResourceRecordType.A:
  505. case DnsResourceRecordType.AAAA:
  506. if (i > 0)
  507. answer += ", ";
  508. answer += additional.Name + " (" + additional.RDATA.ToString() + ")";
  509. break;
  510. }
  511. }
  512. answer += "]";
  513. break;
  514. }
  515. }
  516. }
  517. EDnsClientSubnetOptionData responseECS = response.GetEDnsClientSubnetOption();
  518. if (responseECS is not null)
  519. answer += "; ECS: " + responseECS.Address.ToString() + "/" + responseECS.ScopePrefixLength;
  520. responseInfo = " RCODE: " + response.RCODE.ToString() + "; ANSWER: " + answer;
  521. }
  522. Write(ep, protocol, question + ";" + responseInfo);
  523. }
  524. public void Write(IPEndPoint ep, DnsTransportProtocol protocol, string message)
  525. {
  526. Write(ep, protocol.ToString(), message);
  527. }
  528. public void Write(IPEndPoint ep, string protocol, string message)
  529. {
  530. string ipInfo;
  531. if (ep == null)
  532. ipInfo = "";
  533. else if (ep.Address.IsIPv4MappedToIPv6)
  534. ipInfo = "[" + ep.Address.MapToIPv4().ToString() + ":" + ep.Port + "] ";
  535. else
  536. ipInfo = "[" + ep.ToString() + "] ";
  537. Write(ipInfo + "[" + protocol.ToUpper() + "] " + message);
  538. }
  539. public void Write(string message)
  540. {
  541. if (_enableLogging)
  542. _queue.Add(new LogQueueItem(message));
  543. }
  544. public void DeleteCurrentLogFile()
  545. {
  546. lock (_queueLock)
  547. {
  548. try
  549. {
  550. if (_logOut != null)
  551. _queueCancellationTokenSource.Cancel();
  552. lock (_logFileLock)
  553. {
  554. if (_logOut != null)
  555. _logOut.Dispose();
  556. File.Delete(_logFile);
  557. if (_enableLogging)
  558. StartNewLog();
  559. }
  560. }
  561. finally
  562. {
  563. _queueWait.Set();
  564. }
  565. }
  566. }
  567. public void Save()
  568. {
  569. SaveConfig();
  570. if (_logOut == null)
  571. {
  572. //stopped
  573. if (_enableLogging)
  574. StartLogging();
  575. }
  576. else
  577. {
  578. //running
  579. if (!_enableLogging)
  580. {
  581. StopLogging();
  582. }
  583. else if (!_logFile.StartsWith(ConvertToAbsolutePath(_logFolder)))
  584. {
  585. //log folder changed; restart logging to new folder
  586. StopLogging();
  587. StartLogging();
  588. }
  589. }
  590. }
  591. #endregion
  592. #region properties
  593. public bool EnableLogging
  594. {
  595. get { return _enableLogging; }
  596. set { _enableLogging = value; }
  597. }
  598. public string LogFolder
  599. {
  600. get { return _logFolder; }
  601. set
  602. {
  603. string logFolder;
  604. if (string.IsNullOrEmpty(value))
  605. logFolder = "logs";
  606. else
  607. logFolder = value;
  608. Directory.CreateDirectory(ConvertToAbsolutePath(logFolder));
  609. _logFolder = ConvertToRelativePath(logFolder);
  610. }
  611. }
  612. public int MaxLogFileDays
  613. {
  614. get { return _maxLogFileDays; }
  615. set
  616. {
  617. if (value < 0)
  618. throw new ArgumentOutOfRangeException(nameof(MaxLogFileDays), "MaxLogFileDays must be greater than or equal to 0.");
  619. _maxLogFileDays = value;
  620. if (_maxLogFileDays == 0)
  621. _logCleanupTimer.Change(Timeout.Infinite, Timeout.Infinite);
  622. else
  623. _logCleanupTimer.Change(LOG_CLEANUP_TIMER_INITIAL_INTERVAL, LOG_CLEANUP_TIMER_PERIODIC_INTERVAL);
  624. }
  625. }
  626. public bool UseLocalTime
  627. {
  628. get { return _useLocalTime; }
  629. set { _useLocalTime = value; }
  630. }
  631. public string CurrentLogFile
  632. { get { return _logFile; } }
  633. public string LogFolderAbsolutePath
  634. { get { return ConvertToAbsolutePath(_logFolder); } }
  635. #endregion
  636. class LogQueueItem
  637. {
  638. #region variables
  639. public readonly DateTime _dateTime;
  640. public readonly string _message;
  641. #endregion
  642. #region constructor
  643. public LogQueueItem(string message)
  644. {
  645. _dateTime = DateTime.UtcNow;
  646. _message = message;
  647. }
  648. #endregion
  649. }
  650. }
  651. }