From 4fcf044cc0a4265f6c9c2aad2a3a7aad92eafefa Mon Sep 17 00:00:00 2001 From: Roy Ben Shabat Date: Sun, 17 Aug 2025 19:50:32 +0300 Subject: Telemetry Logs & Events. --- .../Tango.Telemetry/DateTimeUTCFixer.cs | 209 +++++++++++++++++++++ .../Tango.Telemetry/Mappers/EventMapper.cs | 25 +++ .../Tango.Telemetry/Mappers/LogMapper.cs | 42 +++++ .../Sources/TelemetryEventsHistorySource.cs | 79 ++++++++ .../Sources/TelemetryEventsStreamingSource.cs | 64 +++++++ .../Sources/TelemetryLogsStreamingSource.cs | 19 +- .../Tango.Telemetry/Tango.Telemetry.csproj | 6 + .../Tango.Telemetry/Telemetries/TelemetryEvent.cs | 16 ++ .../TelemetryLiteDBStorageManager.cs | 4 + 9 files changed, 448 insertions(+), 16 deletions(-) create mode 100644 Software/Visual_Studio/Tango.Telemetry/DateTimeUTCFixer.cs create mode 100644 Software/Visual_Studio/Tango.Telemetry/Mappers/EventMapper.cs create mode 100644 Software/Visual_Studio/Tango.Telemetry/Mappers/LogMapper.cs create mode 100644 Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryEventsHistorySource.cs create mode 100644 Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryEventsStreamingSource.cs create mode 100644 Software/Visual_Studio/Tango.Telemetry/Telemetries/TelemetryEvent.cs (limited to 'Software/Visual_Studio') diff --git a/Software/Visual_Studio/Tango.Telemetry/DateTimeUTCFixer.cs b/Software/Visual_Studio/Tango.Telemetry/DateTimeUTCFixer.cs new file mode 100644 index 000000000..2a0c3cc85 --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/DateTimeUTCFixer.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Tango.Telemetry +{ + internal static class DateTimeUtcFixer + { + /// + /// Recursively scans the object graph starting at and + /// sets Kind=Utc on any DateTime/DateTime? properties or collection elements that are Kind=Unspecified. + /// Uses DateTime.SpecifyKind(value, DateTimeKind.Utc) (no clock adjustment). + /// Notes: + /// - Only properties are updated (not fields). + /// - Collections: updates elements for IList/arrays and dictionary values; traverses other IEnumerable to reach nested objects. + /// - Read-only properties (no setter) are skipped. + /// - Root objects that are a DateTime value cannot be changed in place (struct); wrap them in a container if needed. + /// + public static void EnsureDateTimeUTC(object obj) + { + var visited = new HashSet(ReferenceEqualityComparer.Instance); + EnsureUtcInternal(obj, visited); + } + + private static void EnsureUtcInternal(object obj, HashSet visited) + { + if (obj == null) return; + + var type = obj.GetType(); + + // Avoid revisiting reference objects (handle cycles, EF proxies, etc.) + if (!type.IsValueType) // only track reference types + { + if (!visited.Add(obj)) return; + } + + // Handle dictionaries: update values, recurse into objects + if (obj is IDictionary dict) + { + var keys = new List(); + foreach (var k in dict.Keys) keys.Add(k); + + foreach (var key in keys) + { + var value = dict[key]; + if (TrySpecifyUtcOnBoxedDateTime(ref value)) + { + dict[key] = value!; + } + else + { + EnsureUtcInternal(value, visited); + } + } + return; + } + + // Handle lists/arrays: update elements, recurse into objects + if (obj is IList list) + { + for (int i = 0; i < list.Count; i++) + { + var element = list[i]; + if (TrySpecifyUtcOnBoxedDateTime(ref element)) + { + list[i] = element!; + } + else + { + EnsureUtcInternal(element, visited); + } + } + return; + } + + // Traverse other enumerables just to reach nested objects (cannot replace DateTime elements) + if (obj is IEnumerable enumerable && !(obj is string)) + { + foreach (var element in enumerable) + { + EnsureUtcInternal(element, visited); + } + } + + // Inspect properties + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + foreach (var prop in type.GetProperties(flags)) + { + // indexers not supported + if (prop.GetIndexParameters().Length != 0) continue; + + // must be readable + if (!prop.CanRead) continue; + + var pType = prop.PropertyType; + + // DateTime + if (pType == typeof(DateTime)) + { + var val = (DateTime)prop.GetValue(obj); + if (val.Kind == DateTimeKind.Unspecified) + { + var newVal = DateTime.SpecifyKind(val, DateTimeKind.Utc); + TrySetProperty(obj, prop, newVal); + } + continue; + } + + // Nullable + var underlying = Nullable.GetUnderlyingType(pType); + if (underlying == typeof(DateTime)) + { + var val = (DateTime?)prop.GetValue(obj); + if (val.HasValue && val.Value.Kind == DateTimeKind.Unspecified) + { + var newVal = (DateTime?)DateTime.SpecifyKind(val.Value, DateTimeKind.Utc); + TrySetProperty(obj, prop, newVal); + } + continue; + } + + // Skip obvious simple types + if (IsSimple(pType)) continue; + + // Recurse into complex objects + var child = prop.GetValue(obj); + EnsureUtcInternal(child, visited); + } + } + + private static bool TrySpecifyUtcOnBoxedDateTime(ref object obj) + { + if (obj == null) return false; + + var t = obj.GetType(); + + // Exact DateTime + if (t == typeof(DateTime)) + { + var dt = (DateTime)obj; + if (dt.Kind == DateTimeKind.Unspecified) + { + obj = DateTime.SpecifyKind(dt, DateTimeKind.Utc); + } + return true; + } + + // Nullable boxed + var underlying = Nullable.GetUnderlyingType(t); + if (underlying == typeof(DateTime)) + { + // boxed Nullable can be unboxed to DateTime? safely + var ndt = (DateTime?)obj; + if (ndt.HasValue && ndt.Value.Kind == DateTimeKind.Unspecified) + { + obj = (DateTime?)DateTime.SpecifyKind(ndt.Value, DateTimeKind.Utc); + } + return true; + } + + return false; + } + + private static bool TrySetProperty(object target, PropertyInfo prop, object value) + { + try + { + // Prefer invoking the setter (even if non-public) + var setter = prop.GetSetMethod(nonPublic: true); + if (setter == null) return false; + setter.Invoke(target, new[] { value }); + return true; + } + catch + { + // Fall back to PropertyInfo.SetValue (may still work in some runtimes) + try + { + prop.SetValue(target, value); + return true; + } + catch + { + return false; + } + } + } + + private static bool IsSimple(Type t) + { + if (t.IsPrimitive || t.IsEnum) return true; + if (t == typeof(string) || t == typeof(decimal) || t == typeof(Guid) || + t == typeof(Uri) || t == typeof(TimeSpan) || t == typeof(DateTimeOffset)) + return true; + // Value types other than DateTime/Nullable are considered simple + return t.IsValueType; + } + + /// Reference equality comparer for the visited set. + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + public static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer(); + public new bool Equals(object x, object y) => ReferenceEquals(x, y); + public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj); + } + } +} diff --git a/Software/Visual_Studio/Tango.Telemetry/Mappers/EventMapper.cs b/Software/Visual_Studio/Tango.Telemetry/Mappers/EventMapper.cs new file mode 100644 index 000000000..02969d5e3 --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/Mappers/EventMapper.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Tango.BL.Entities; +using Tango.Telemetry.Telemetries; + +namespace Tango.Telemetry.Mappers +{ + public class EventMapper + { + public static TelemetryEvent MapEvent(MachinesEvent ev) + { + TelemetryEvent t = new TelemetryEvent(); + t.ID = ev.Guid; + t.Time = ev.DateTime; + t.HostName = ev.HostName; + t.EventTypeGuid = ev.EventTypeGuid; + t.Description = ev.Description; + + return t; + } + } +} diff --git a/Software/Visual_Studio/Tango.Telemetry/Mappers/LogMapper.cs b/Software/Visual_Studio/Tango.Telemetry/Mappers/LogMapper.cs new file mode 100644 index 000000000..6f5abbf1b --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/Mappers/LogMapper.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Tango.Logging; +using Tango.PMR.Debugging; +using Tango.Telemetry.Telemetries; + +namespace Tango.Telemetry.Mappers +{ + public class LogMapper + { + public static TelemetryLog MapLog(LogItemBase log) + { + TelemetryLog tLog = new TelemetryLog(); + tLog.Source = "Application"; + tLog.Time = DateTime.UtcNow; + tLog.Category = log.Category.ToString(); + tLog.Class = log.ClassName; + tLog.Method = log.CallerMethodName; + tLog.Line = log.CallerLineNumber; + tLog.Message = log.Message; + + return tLog; + } + + public static TelemetryLog MapLog(StartDebugLogResponse log) + { + TelemetryLog tLog = new TelemetryLog(); + tLog.Source = "Firmware"; + tLog.Time = DateTime.UtcNow; + tLog.Category = log.Category.ToString(); + tLog.Class = log.FileName; + tLog.Method = $"[{log.ModuleId}] [{log.Filter}] [{log.Parameter}]"; + tLog.Line = log.LineNumber; + tLog.Message = log.Message; + + return tLog; + } + } +} diff --git a/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryEventsHistorySource.cs b/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryEventsHistorySource.cs new file mode 100644 index 000000000..57abee66b --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryEventsHistorySource.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Data.Entity; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Tango.BL; +using Tango.Telemetry.Mappers; +using Tango.Telemetry.Telemetries; + +namespace Tango.Telemetry.Sources +{ + public class TelemetryEventsHistorySource : ITelemetryHistorySource + { + private bool _isBusy; + + public TelemetryHistorySourceDirection Direction { get; } = TelemetryHistorySourceDirection.Descending; + public string Name { get; } = "Events History"; + public bool RequiresTelemetryDuplicationTracking { get; } = true; + + public async Task CanRequestHistory(DateTime from) + { + if (_isBusy) + { + return false; + } + else + { + try + { + using (ObservablesContext db = ObservablesContext.CreateDefault()) + { + return await db.MachinesEvents.CountAsync(x => x.LastUpdated < from) > 0; + } + } + catch + { + return false; + } + } + } + + public async Task> RequestHistory(DateTime from) + { + try + { + _isBusy = true; + + using (ObservablesContext db = ObservablesContext.CreateDefault()) + { + var events = await db.MachinesEvents + .OrderByDescending(x => x.LastUpdated) + .Where(x => x.LastUpdated < from) + .Take(10) + .ToListAsync(); + + List tRuns = new List(); + + foreach (var ev in events) + { + var tEvent = EventMapper.MapEvent(ev); + tRuns.Add(tEvent); + } + + return tRuns; + } + } + finally + { + _isBusy = false; + } + } + + public void Dispose() + { + + } + } +} diff --git a/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryEventsStreamingSource.cs b/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryEventsStreamingSource.cs new file mode 100644 index 000000000..4b00c49ce --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryEventsStreamingSource.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Tango.BL.Entities; +using Tango.Integration.Operation; +using Tango.Telemetry.Mappers; +using Tango.Telemetry.Telemetries; + +namespace Tango.Telemetry.Sources +{ + public class TelemetryEventsStreamingSource : ITelemetryStreamingSource + { + private IMachineOperator _machineOperator; + + public bool IsStarted { get; private set; } + public string Name { get; } = "Events Streaming"; + public bool RequiresTelemetryDuplicationTracking { get; } = true; + + public event EventHandler TelemetryAvailable; + + public TelemetryEventsStreamingSource(IMachineOperator machineOperator) + { + _machineOperator = machineOperator; + } + + private void MachineEventsStateProvider_NewEvents(object sender, IEnumerable events) + { + if (IsStarted) + { + foreach (var ev in events) + { + TelemetryEvent t = EventMapper.MapEvent(ev); + + TelemetryAvailable?.Invoke(this, new TelemetryAvailableEventArgs() { TelemetryObject = t }); + } + } + } + + public void Start() + { + if (!IsStarted) + { + IsStarted = true; + _machineOperator.MachineEventsStateProvider.NewEvents += MachineEventsStateProvider_NewEvents; + } + } + + public void Stop() + { + if (IsStarted) + { + IsStarted = false; + _machineOperator.MachineEventsStateProvider.NewEvents -= MachineEventsStateProvider_NewEvents; + } + } + + public void Dispose() + { + Stop(); + } + } +} diff --git a/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryLogsStreamingSource.cs b/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryLogsStreamingSource.cs index 9deb93132..2c8020f7c 100644 --- a/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryLogsStreamingSource.cs +++ b/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryLogsStreamingSource.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using Tango.Integration.Operation; using Tango.Logging; +using Tango.Telemetry.Mappers; using Tango.Telemetry.Telemetries; namespace Tango.Telemetry.Sources @@ -53,14 +54,7 @@ namespace Tango.Telemetry.Sources _lastFirmwareMessage = new KeyValuePair(DateTime.UtcNow, log.Message); - TelemetryLog tLog = new TelemetryLog(); - tLog.Source = "Firmware"; - tLog.Time = DateTime.UtcNow; - tLog.Category = log.Category.ToString(); - tLog.Class = log.FileName; - tLog.Method = $"[{log.ModuleId}] [{log.Filter}] [{log.Parameter}]"; - tLog.Line = log.LineNumber; - tLog.Message = log.Message; + TelemetryLog tLog = LogMapper.MapLog(log); TelemetryAvailable?.Invoke(this, new TelemetryAvailableEventArgs() { TelemetryObject = tLog }); } } @@ -71,14 +65,7 @@ namespace Tango.Telemetry.Sources { if (!Config.Categories.Contains(log.Category)) return; - TelemetryLog tLog = new TelemetryLog(); - tLog.Source = "Application"; - tLog.Time = log.TimeStamp.ToUniversalTime(); - tLog.Category = log.Category.ToString(); - tLog.Class = log.ClassName; - tLog.Method = log.CallerMethodName; - tLog.Line = log.CallerLineNumber; - tLog.Message = log.Message; + TelemetryLog tLog = LogMapper.MapLog(log); TelemetryAvailable?.Invoke(this, new TelemetryAvailableEventArgs() { TelemetryObject = tLog }); } } diff --git a/Software/Visual_Studio/Tango.Telemetry/Tango.Telemetry.csproj b/Software/Visual_Studio/Tango.Telemetry/Tango.Telemetry.csproj index 35ddf9839..14cd6e904 100644 --- a/Software/Visual_Studio/Tango.Telemetry/Tango.Telemetry.csproj +++ b/Software/Visual_Studio/Tango.Telemetry/Tango.Telemetry.csproj @@ -239,6 +239,7 @@ + @@ -250,7 +251,9 @@ + + @@ -258,6 +261,8 @@ + + @@ -266,6 +271,7 @@ + diff --git a/Software/Visual_Studio/Tango.Telemetry/Telemetries/TelemetryEvent.cs b/Software/Visual_Studio/Tango.Telemetry/Telemetries/TelemetryEvent.cs new file mode 100644 index 000000000..2b11b7922 --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/Telemetries/TelemetryEvent.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tango.Telemetry.Telemetries +{ + [TelemetryName("Event", 1)] + public class TelemetryEvent : TelemetryBase + { + public String HostName { get; set; } + public String EventTypeGuid { get; set; } + public String Description { get; set; } + } +} diff --git a/Software/Visual_Studio/Tango.Telemetry/TelemetryLiteDBStorageManager.cs b/Software/Visual_Studio/Tango.Telemetry/TelemetryLiteDBStorageManager.cs index 70b1f6f18..f92d508e7 100644 --- a/Software/Visual_Studio/Tango.Telemetry/TelemetryLiteDBStorageManager.cs +++ b/Software/Visual_Studio/Tango.Telemetry/TelemetryLiteDBStorageManager.cs @@ -147,6 +147,10 @@ namespace Tango.Telemetry lock (_lock) { var collection = GetPendingTelemetriesCollection(); + + //Ensure all datetimes "Kind" is UTC so LiteDB won't change them on query. + DateTimeUtcFixer.EnsureDateTimeUTC(pendingTelemetry.TelemetryObject); + collection.Upsert(pendingTelemetry); } } -- cgit v1.3.1