|
@@ -1,32 +1,60 @@
|
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Concurrent;
|
|
|
|
|
+using System.IO.Abstractions;
|
|
|
using System.Net;
|
|
using System.Net;
|
|
|
using System.Net.Sockets;
|
|
using System.Net.Sockets;
|
|
|
using System.Text;
|
|
using System.Text;
|
|
|
using System.Text.Json;
|
|
using System.Text.Json;
|
|
|
|
|
+using System.Text.Json.Serialization;
|
|
|
using Quadarax.Foundation.Core.Logging;
|
|
using Quadarax.Foundation.Core.Logging;
|
|
|
|
|
|
|
|
namespace qmonlib
|
|
namespace qmonlib
|
|
|
{
|
|
{
|
|
|
public delegate void GeneralDataReceivedDelegate(MonGeneral general);
|
|
public delegate void GeneralDataReceivedDelegate(MonGeneral general);
|
|
|
|
|
+ public delegate void DataReceivedDelegate(MonData[] data);
|
|
|
public class QMonReceiver : QMonHostBase<QMonReceiverConfiguration>
|
|
public class QMonReceiver : QMonHostBase<QMonReceiverConfiguration>
|
|
|
{
|
|
{
|
|
|
#region *** Properties ***
|
|
#region *** Properties ***
|
|
|
protected override Uri UriGeneral => Configuration.SourceUriGeneral;
|
|
protected override Uri UriGeneral => Configuration.SourceUriGeneral;
|
|
|
protected override Uri UriData => Configuration.SourceUriData;
|
|
protected override Uri UriData => Configuration.SourceUriData;
|
|
|
protected override TimeSpan IntervalGeneral => Configuration.GeneralReceiverInterval;
|
|
protected override TimeSpan IntervalGeneral => Configuration.GeneralReceiverInterval;
|
|
|
- protected override TimeSpan? IntervalData => null;
|
|
|
|
|
|
|
+ protected override TimeSpan? IntervalData => Configuration.DataCache.FlushCacheInterval;
|
|
|
public MonGeneral[] AvailableGeneral => _generalCache.Values.Select(x=>x.Data).ToArray();
|
|
public MonGeneral[] AvailableGeneral => _generalCache.Values.Select(x=>x.Data).ToArray();
|
|
|
#endregion
|
|
#endregion
|
|
|
#region *** Fields ***
|
|
#region *** Fields ***
|
|
|
private GeneralDataReceivedDelegate _generalReceivedCallback;
|
|
private GeneralDataReceivedDelegate _generalReceivedCallback;
|
|
|
|
|
+ private DataReceivedDelegate _dataReceivedCallback;
|
|
|
private bool _isReceivingGeneral;
|
|
private bool _isReceivingGeneral;
|
|
|
private CancellationTokenSource _dataReceiverCancel;
|
|
private CancellationTokenSource _dataReceiverCancel;
|
|
|
private IDictionary<string, MonReceiverGeneralCahedItem> _generalCache = new ConcurrentDictionary<string, MonReceiverGeneralCahedItem>();
|
|
private IDictionary<string, MonReceiverGeneralCahedItem> _generalCache = new ConcurrentDictionary<string, MonReceiverGeneralCahedItem>();
|
|
|
|
|
+ private IList<MonData> _dataCache = new List<MonData>();
|
|
|
|
|
+ private JsonSerializerOptions _jsonSerializerOptions;
|
|
|
|
|
+ private DateTime _lastSavedTimestamp;
|
|
|
|
|
+ private IFileSystem _fileSystem;
|
|
|
#endregion
|
|
#endregion
|
|
|
#region *** Constructors ***
|
|
#region *** Constructors ***
|
|
|
- public QMonReceiver(QMonReceiverConfiguration configuration, ILogger logger, GeneralDataReceivedDelegate generalDataReceivedCallback = null) : base(configuration, logger)
|
|
|
|
|
|
|
+ public QMonReceiver(IFileSystem fileSystem, QMonReceiverConfiguration configuration, ILogger logger, GeneralDataReceivedDelegate generalDataReceivedCallback = null, DataReceivedDelegate liveDataReceivedCallback = null) : base(configuration, logger)
|
|
|
{
|
|
{
|
|
|
|
|
+ _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
|
|
|
|
|
+ _lastSavedTimestamp = DateTime.Now;
|
|
|
_generalReceivedCallback = generalDataReceivedCallback;
|
|
_generalReceivedCallback = generalDataReceivedCallback;
|
|
|
|
|
+ _dataReceivedCallback = liveDataReceivedCallback;
|
|
|
|
|
+ _jsonSerializerOptions = new JsonSerializerOptions
|
|
|
|
|
+ {
|
|
|
|
|
+ IgnoreNullValues = true,
|
|
|
|
|
+ WriteIndented = false,
|
|
|
|
|
+ PropertyNameCaseInsensitive = true,
|
|
|
|
|
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
|
|
|
+ MaxDepth = 10
|
|
|
|
|
+ };
|
|
|
|
|
+ _jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
|
|
|
|
+ Configuration.DataCache.Path = Configuration.DataCache.Path.Replace("%AppData%",
|
|
|
|
|
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));
|
|
|
|
|
+ if (!_fileSystem.Directory.Exists(Configuration.DataCache.Path))
|
|
|
|
|
+ {
|
|
|
|
|
+ _fileSystem.Directory.CreateDirectory(Configuration.DataCache.Path);
|
|
|
|
|
+ Log(LogSeverityEnum.Debug,$"Data receiver created data cache directory '{Configuration.DataCache.Path}'");
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
#endregion
|
|
#endregion
|
|
|
|
|
|
|
@@ -40,12 +68,15 @@ namespace qmonlib
|
|
|
{
|
|
{
|
|
|
try
|
|
try
|
|
|
{
|
|
{
|
|
|
- OnThreadData(_dataReceiverCancel.Token);
|
|
|
|
|
|
|
+ Log(LogSeverityEnum.Debug, "Data receiver thread started.");
|
|
|
|
|
+ while (!_dataReceiverCancel.Token.IsCancellationRequested)
|
|
|
|
|
+ OnThreadData(_dataReceiverCancel.Token);
|
|
|
}
|
|
}
|
|
|
catch (OperationCanceledException)
|
|
catch (OperationCanceledException)
|
|
|
{
|
|
{
|
|
|
Log(LogSeverityEnum.Info, $"Data receiver thread on {UriData} unexpected stopped");
|
|
Log(LogSeverityEnum.Info, $"Data receiver thread on {UriData} unexpected stopped");
|
|
|
}
|
|
}
|
|
|
|
|
+ Log(LogSeverityEnum.Debug, "Data receiver thread stoped.");
|
|
|
}).Start();
|
|
}).Start();
|
|
|
Log(LogSeverityEnum.Info, $"Data receiver thread on {UriData} started");
|
|
Log(LogSeverityEnum.Info, $"Data receiver thread on {UriData} started");
|
|
|
}
|
|
}
|
|
@@ -58,7 +89,37 @@ namespace qmonlib
|
|
|
|
|
|
|
|
protected override void OnTimerData(object? state)
|
|
protected override void OnTimerData(object? state)
|
|
|
{
|
|
{
|
|
|
- throw new NotSupportedException("Timer is not allowed in receiver.");
|
|
|
|
|
|
|
+ _lastSavedTimestamp = DateTime.Now;
|
|
|
|
|
+ var itemsToSave = _dataCache.Where(x => x.Timestamp < _lastSavedTimestamp).ToArray();
|
|
|
|
|
+ var instances = itemsToSave.Select(x => x.InstanceIdentifier).Distinct();
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var instance in instances)
|
|
|
|
|
+ {
|
|
|
|
|
+ var dirName = _fileSystem.Path.Combine(Configuration.DataCache.Path, instance);
|
|
|
|
|
+ if (_fileSystem.Directory.Exists(dirName))
|
|
|
|
|
+ {
|
|
|
|
|
+ _fileSystem.Directory.CreateDirectory(dirName);
|
|
|
|
|
+ Log(LogSeverityEnum.Debug,$"Data receiver created data cache directory '{dirName}'");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var batchData = itemsToSave.Where(x => x.InstanceIdentifier == instance).OrderBy(x=>x.Timestamp).ToArray();
|
|
|
|
|
+ _dataReceivedCallback?.Invoke(batchData);
|
|
|
|
|
+
|
|
|
|
|
+ var dfrom = batchData.Min(x => x.Timestamp).ToFileTimeUtc();
|
|
|
|
|
+ var dto = batchData.Max(x => x.Timestamp).ToFileTimeUtc();
|
|
|
|
|
+ var sbFileName = new StringBuilder();
|
|
|
|
|
+ sbFileName.Append("mondata_").Append(dfrom).Append("-").Append(dto).Append(".json");
|
|
|
|
|
+ var fileName = _fileSystem.Path.Combine(dirName, sbFileName.ToString());
|
|
|
|
|
+ using (var streamWriter = File.CreateText(fileName))
|
|
|
|
|
+ {
|
|
|
|
|
+ JsonSerializer.Serialize(streamWriter.BaseStream, batchData, _jsonSerializerOptions);
|
|
|
|
|
+ streamWriter.Flush();
|
|
|
|
|
+ }
|
|
|
|
|
+ Log(LogSeverityEnum.Trace, $"Data receiver created dump data file '{fileName}'");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var item in itemsToSave)
|
|
|
|
|
+ _dataCache.Remove(item);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
protected override void OnTimerGeneral(object? state)
|
|
protected override void OnTimerGeneral(object? state)
|
|
@@ -101,7 +162,13 @@ namespace qmonlib
|
|
|
if (UdpClientData.Available==0) return;
|
|
if (UdpClientData.Available==0) return;
|
|
|
var remoteEndPoint = new IPEndPoint(IPAddress.Parse(this.UriData.DnsSafeHost), this.UriData.Port);
|
|
var remoteEndPoint = new IPEndPoint(IPAddress.Parse(this.UriData.DnsSafeHost), this.UriData.Port);
|
|
|
var data = UdpClientData.Receive(ref remoteEndPoint);
|
|
var data = UdpClientData.Receive(ref remoteEndPoint);
|
|
|
- var jsonData = JsonDocument.Parse(Encoding.UTF8.GetString(data));
|
|
|
|
|
|
|
+ var monData = JsonSerializer.Deserialize<MonData>(data, _jsonSerializerOptions);
|
|
|
|
|
+ if (monData == null)
|
|
|
|
|
+ {
|
|
|
|
|
+ Log(LogSeverityEnum.Warn, $"[ReceiverData] cannot deserialize monitor data from '{remoteEndPoint}' size={data.Length} bytes. Skipped.");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ _dataCache.Add(monData);
|
|
|
Log(LogSeverityEnum.Debug, $"[ReceiverData] receive monitor data from '{remoteEndPoint}' size={data.Length} bytes.");
|
|
Log(LogSeverityEnum.Debug, $"[ReceiverData] receive monitor data from '{remoteEndPoint}' size={data.Length} bytes.");
|
|
|
}
|
|
}
|
|
|
catch (Exception e)
|
|
catch (Exception e)
|
|
@@ -123,6 +190,10 @@ namespace qmonlib
|
|
|
_dataReceiverCancel = null;
|
|
_dataReceiverCancel = null;
|
|
|
base.OnDisposing();
|
|
base.OnDisposing();
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
#endregion
|
|
#endregion
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|