From 0df9f37075dd697ac34f4ed2a2749f62aa27a654 Mon Sep 17 00:00:00 2001 From: Roy Ben Shabat Date: Sat, 2 Aug 2025 21:38:19 +0300 Subject: Telemetry Testing. --- .../Destinations/AzureHubTelemetryDestination.cs | 77 --- .../Destinations/MqttTelemetryDestination.cs | 130 ---- .../Destinations/TelemetryAzureHubDestination.cs | 188 ++++++ .../Destinations/TelemetryMqttDestination.cs | 129 ++++ .../Tango.Telemetry/Docs/AiResponse.md | 138 +++++ .../Visual_Studio/Tango.Telemetry/ITelemetry.cs | 1 + .../ITelemetryCheckpointsRecoveryClient.cs | 14 + .../Tango.Telemetry/ITelemetryDestination.cs | 25 +- .../Tango.Telemetry/ITelemetryPublisher.cs | 85 ++- .../Tango.Telemetry/ITelemetrySource.cs | 1 + .../Tango.Telemetry/ITelemetryStorageManager.cs | 72 +++ .../Reporting/DestinationStatusSummary.cs | 36 ++ .../Tango.Telemetry/Reporting/SourceSummary.cs | 31 + .../Tango.Telemetry/Reporting/SourceTypeSummary.cs | 33 + .../Tango.Telemetry/Reporting/TelemetryReport.cs | 86 +++ .../Sources/TelemetryDiagnosticsSource.cs | 4 +- .../Sources/TelemetryJobRunsHistoryModule.cs | 61 -- .../Sources/TelemetryJobRunsHistorySource.cs | 65 ++ .../Tango.Telemetry/Tango.Telemetry.csproj | 65 +- .../Tango.Telemetry/Telemetries/TelemetryJobRun.cs | 23 + .../Visual_Studio/Tango.Telemetry/TelemetryBase.cs | 12 +- .../TelemetryLiteDBStorageManager.cs | 193 +++++- .../TelemetryPendingStorageSource.cs | 1 + .../Tango.Telemetry/TelemetryPublishPackage.cs | 18 +- .../Tango.Telemetry/TelemetryPublishResult.cs | 81 ++- .../TelemetryPublishResultAvailableEventArgs.cs | 13 + .../Tango.Telemetry/TelemetryPublisher.cs | 679 +++++++++++++++++---- .../Tango.Telemetry/TelemetryPublisherAdvanced.cs | 93 +-- .../TelemetryPublisherConfiguration.cs | 54 +- .../Tango.Telemetry/TelemetrySourceTypes.cs | 17 +- Software/Visual_Studio/Tango.Telemetry/app.config | 59 ++ .../Visual_Studio/Tango.Telemetry/packages.config | 23 +- Software/Visual_Studio/Tango.sln | 369 +++++++++++ .../Tango.Telemetry.Tester.IOT.CLI/App.config | 34 ++ .../Tango.Telemetry.Tester.IOT.CLI/Program.cs | 107 ++++ .../Properties/AssemblyInfo.cs | 36 ++ .../Tango.Telemetry.Tester.IOT.CLI.csproj | 202 ++++++ .../Tango.Telemetry.Tester.IOT.CLI/packages.config | 77 +++ .../Utilities/Tango.TelemetryTester.CLI/App.config | 62 ++ .../Tango.TelemetryTester.CLI/DatabaseHelper.cs | 33 + .../Utilities/Tango.TelemetryTester.CLI/Logger.cs | 49 ++ .../MockCheckpointsRecoveryClient.cs | 13 + .../MockDestinationWithFailure.cs | 46 ++ .../Tango.TelemetryTester.CLI/MockHistorySource.cs | 35 ++ .../MockStreamingSource.cs | 45 ++ .../Tango.TelemetryTester.CLI/MockTelemetry.cs | 21 + .../Utilities/Tango.TelemetryTester.CLI/Program.cs | 120 ++++ .../Properties/AssemblyInfo.cs | 36 ++ .../Tango.TelemetryTester.CLI.csproj | 83 +++ .../Verifications List.md | 90 +++ .../Tango.TelemetryTester.CLI/packages.config | 4 + 51 files changed, 3504 insertions(+), 465 deletions(-) delete mode 100644 Software/Visual_Studio/Tango.Telemetry/Destinations/AzureHubTelemetryDestination.cs delete mode 100644 Software/Visual_Studio/Tango.Telemetry/Destinations/MqttTelemetryDestination.cs create mode 100644 Software/Visual_Studio/Tango.Telemetry/Destinations/TelemetryAzureHubDestination.cs create mode 100644 Software/Visual_Studio/Tango.Telemetry/Destinations/TelemetryMqttDestination.cs create mode 100644 Software/Visual_Studio/Tango.Telemetry/Docs/AiResponse.md create mode 100644 Software/Visual_Studio/Tango.Telemetry/ITelemetryCheckpointsRecoveryClient.cs create mode 100644 Software/Visual_Studio/Tango.Telemetry/Reporting/DestinationStatusSummary.cs create mode 100644 Software/Visual_Studio/Tango.Telemetry/Reporting/SourceSummary.cs create mode 100644 Software/Visual_Studio/Tango.Telemetry/Reporting/SourceTypeSummary.cs create mode 100644 Software/Visual_Studio/Tango.Telemetry/Reporting/TelemetryReport.cs delete mode 100644 Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryJobRunsHistoryModule.cs create mode 100644 Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryJobRunsHistorySource.cs create mode 100644 Software/Visual_Studio/Tango.Telemetry/TelemetryPublishResultAvailableEventArgs.cs create mode 100644 Software/Visual_Studio/Tango.Telemetry/app.config create mode 100644 Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/App.config create mode 100644 Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Program.cs create mode 100644 Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Properties/AssemblyInfo.cs create mode 100644 Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Tango.Telemetry.Tester.IOT.CLI.csproj create mode 100644 Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/packages.config create mode 100644 Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/App.config create mode 100644 Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/DatabaseHelper.cs create mode 100644 Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Logger.cs create mode 100644 Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockCheckpointsRecoveryClient.cs create mode 100644 Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockDestinationWithFailure.cs create mode 100644 Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockHistorySource.cs create mode 100644 Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockStreamingSource.cs create mode 100644 Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockTelemetry.cs create mode 100644 Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Program.cs create mode 100644 Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Properties/AssemblyInfo.cs create mode 100644 Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Tango.TelemetryTester.CLI.csproj create mode 100644 Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Verifications List.md create mode 100644 Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/packages.config (limited to 'Software/Visual_Studio') diff --git a/Software/Visual_Studio/Tango.Telemetry/Destinations/AzureHubTelemetryDestination.cs b/Software/Visual_Studio/Tango.Telemetry/Destinations/AzureHubTelemetryDestination.cs deleted file mode 100644 index 7a9cc9f7a..000000000 --- a/Software/Visual_Studio/Tango.Telemetry/Destinations/AzureHubTelemetryDestination.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Microsoft.Azure.Devices.Client; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Tango.Core; - -namespace Tango.Telemetry.Destinations -{ - public class AzureHubTelemetryDestination : ExtendedObject, ITelemetryDestination - { - private DeviceClient _hubClient; - - public string Name { get; set; } = "Azure IoT Hub"; - public bool Enable { get; set; } = true; - public String ConnectionString { get; private set; } - - public ConnectionStatus HubConnectionStatus { get; private set; } - public IReadOnlyList SupportedSourceTypes { get; private set; } - - public AzureHubTelemetryDestination(String connectionString) - { - HubConnectionStatus = ConnectionStatus.Connected; - ConnectionString = connectionString; - SupportedSourceTypes = new List() { TelemetrySourceTypes.PendingStorage, TelemetrySourceTypes.Streaming, TelemetrySourceTypes.ExternalStorage }; - } - - public Task IsAvailable() - { - if (_hubClient == null) - { - return Task.FromResult(true); - } - else - { - return Task.FromResult(HubConnectionStatus == ConnectionStatus.Connected); - } - } - - public async Task Publish(TelemetryPublishPackage package, List> properties) - { - if (_hubClient == null) - { - _hubClient = DeviceClient.CreateFromConnectionString(ConnectionString, TransportType.Mqtt); - _hubClient.SetConnectionStatusChangesHandler((status, reason) => - { - HubConnectionStatus = status; - LogManager.Log($"IoT hub status changed to: {status}, Reason: {reason}."); - }); - } - - var message = new Message(Encoding.UTF8.GetBytes(package.ToPayload())) - { - ContentType = "application/json", - ContentEncoding = "utf-8" - }; - - foreach (var prop in properties) - { - message.Properties.Add(prop.Key, prop.Value); - } - - await _hubClient.SendEventAsync(message); - } - - public void Dispose() - { - _hubClient?.Dispose(); - } - - public override string ToString() - { - return Name; - } - } -} diff --git a/Software/Visual_Studio/Tango.Telemetry/Destinations/MqttTelemetryDestination.cs b/Software/Visual_Studio/Tango.Telemetry/Destinations/MqttTelemetryDestination.cs deleted file mode 100644 index b5ff05c29..000000000 --- a/Software/Visual_Studio/Tango.Telemetry/Destinations/MqttTelemetryDestination.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Collections; -using MQTTnet.Client; -using MQTTnet.Client.Options; -using MQTTnet; -using System.Reflection; -using MQTTnet.Client.Connecting; -using Tango.Core; -using MQTTnet.Packets; - -namespace Tango.Telemetry.Destinations -{ - public class MqttTelemetryDestination : ExtendedObject, ITelemetryDestination - { - private IMqttClient _mqttClient; - private IMqttClientOptions _mqttOptions; - private DateTime _nextRealAvailabilityCheck; - - public string Name { get; set; } = "MQTT"; - public bool Enable { get; set; } = true; - public String Address { get; private set; } - public int Port { get; private set; } - public String Topic { get; private set; } - - public IReadOnlyList SupportedSourceTypes { get; private set; } - - /// - /// - /// - /// e.g machie/telemetry/serial number - /// Default localhost - /// Default 1883 - public MqttTelemetryDestination(String topic, String address = "localhost", int port = 1883) - { - _nextRealAvailabilityCheck = DateTime.Now; - Topic = topic; - Address = address; - Port = port; - SupportedSourceTypes = new List() { TelemetrySourceTypes.Streaming }; - } - - public async Task IsAvailable() - { - if (_mqttClient == null) - { - return await EnsureConnection(); - } - else - { - if (DateTime.Now > _nextRealAvailabilityCheck) - { - _nextRealAvailabilityCheck = DateTime.Now.AddMinutes(5); - return await EnsureConnection(); - } - else - { - return _mqttClient.IsConnected; - } - } - } - - private async Task EnsureConnection() - { - if (_mqttClient == null || !_mqttClient.IsConnected) - { - try - { - var factory = new MqttFactory(); - _mqttClient = factory.CreateMqttClient(); - - String exeName = Assembly.GetEntryAssembly().GetName().FullName; - _mqttOptions = new MqttClientOptionsBuilder() - .WithClientId(exeName) - .WithTcpServer(Address, Port) - .WithCleanSession() - .Build(); - - var result = await _mqttClient.ConnectAsync(_mqttOptions); - if (result.ResultCode != MqttClientConnectResultCode.Success) - { - LogManager.Log(new Exception($"Error connecting to MQTT broker. {result.ResultCode}")); - return false; - } - } - catch (Exception ex) - { - LogManager.Log(ex, "Error connecting to MQTT broker."); - return false; - } - } - - return true; - } - - public async Task Publish(TelemetryPublishPackage package, List> properties) - { - if (await EnsureConnection()) - { - var message = new MqttApplicationMessageBuilder() - .WithTopic($"{Topic}/{package.PendingTelemetry.TelemetryObject.ToTelemetryName()}") - .WithPayload(package.ToPayload()) - .WithExactlyOnceQoS() - .WithRetainFlag(false) - .Build(); - - foreach (var prop in properties) - { - message.UserProperties.Add(new MqttUserProperty(prop.Key, prop.Value)); - } - - - await _mqttClient.PublishAsync(message); - } - } - - public void Dispose() - { - _mqttClient?.Dispose(); - } - - public override string ToString() - { - return $"{Name} -> {Address}:{Port}"; - } - } -} diff --git a/Software/Visual_Studio/Tango.Telemetry/Destinations/TelemetryAzureHubDestination.cs b/Software/Visual_Studio/Tango.Telemetry/Destinations/TelemetryAzureHubDestination.cs new file mode 100644 index 000000000..01ad31432 --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/Destinations/TelemetryAzureHubDestination.cs @@ -0,0 +1,188 @@ +using Microsoft.Azure.Devices.Client; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Tango.Core; +using Tango.Logging; + +namespace Tango.Telemetry.Destinations +{ + /// + /// Represents a telemetry destination that publishes telemetry data to Azure IoT Hub using MQTT transport. + /// Supports batching, source type filtering, and connection status tracking. + /// + public class TelemetryAzureHubDestination : ExtendedObject, ITelemetryDestination + { + private DeviceClient _hubClient; + private int _batchSize; + private ConcurrentList _batch; + + private class ADXPAckage + { + public String Type { get; set; } + public String Environment { get; set; } + public String MachineType { get; set; } + public String SerialNumber { get; set; } + public ITelemetry Telemetry { get; set; } + public DateTime UploadTime { get; set; } + + public String ToPayload() + { + return JsonConvert.SerializeObject(this, Formatting.None); + } + } + + /// + /// Gets or sets the name of the destination. + /// + public string Name { get; set; } = "Azure IoT Hub"; + + /// + /// Gets the connection string used to connect to Azure IoT Hub. + /// + public string ConnectionString { get; private set; } + + /// + /// Gets the current connection status of the Azure IoT Hub client. + /// + public ConnectionStatus HubConnectionStatus { get; private set; } + + /// + /// Gets the source types supported by this destination. + /// + public IReadOnlyList SupportedSourceTypes { get; private set; } + + /// + /// Gets or sets the maximum number of messages to send in a single batch. + /// The value is clamped between 1 and 100. + /// + public int BatchSize { get => _batchSize; set => _batchSize = Math.Min(Math.Max(1, value), 10); } + + /// + /// Prevents a default instance of the class from being created. + /// + private TelemetryAzureHubDestination() + { + _batch = new ConcurrentList(); + HubConnectionStatus = ConnectionStatus.Connected; + SupportedSourceTypes = new List() + { + TelemetrySourceTypes.PendingStorage, + TelemetrySourceTypes.Streaming, + TelemetrySourceTypes.ExternalStorage + }; + BatchSize = 1; + } + + /// + /// Initializes a new instance of the class with the specified connection string. + /// + /// The Azure IoT Hub connection string. + public TelemetryAzureHubDestination(string connectionString) : this() + { + ConnectionString = connectionString; + } + + /// + /// Determines whether the destination is currently available for publishing. + /// + /// True if the destination is available; otherwise, false. + public Task IsAvailable() + { + if (_hubClient == null) + { + return Task.FromResult(true); + } + else + { + return Task.FromResult(HubConnectionStatus == ConnectionStatus.Connected); + } + } + + /// + /// Publishes a telemetry package to Azure IoT Hub. + /// Supports batching when is greater than 1. + /// + /// The telemetry package to publish. + /// A list of properties to include with the message. + public async Task Publish(TelemetryPublishPackage package, List> properties) + { + if (_hubClient == null) + { + _hubClient = DeviceClient.CreateFromConnectionString(ConnectionString, TransportType.Mqtt); + _hubClient.SetConnectionStatusChangesHandler((status, reason) => + { + HubConnectionStatus = status; + LogManager.Log($"IoT hub status changed to: {status}, Reason: {reason}.", LogCategory.Info); + }); + } + + ADXPAckage adxPackage = new ADXPAckage(); + adxPackage.Type = package.TelemetryName; + adxPackage.Environment = package.Environment; + adxPackage.MachineType = package.MachineType; + adxPackage.SerialNumber = package.SerialNumber; + adxPackage.UploadTime = DateTime.UtcNow; + adxPackage.Telemetry = package.PendingTelemetry.TelemetryObject; + + var message = new Message(Encoding.UTF8.GetBytes(adxPackage.ToPayload())) + { + ContentType = "application/json", + ContentEncoding = "utf-8" + }; + + foreach (var prop in properties) + { + message.Properties.Add(prop.Key, prop.Value); + } + + if (BatchSize > 1) + { + _batch.Add(message); + + if (_batch.Count >= BatchSize) + { + LogManager.Log($"Sending telemetry batch of {_batch.Count} messages to Azure IoT Hub.", LogCategory.Debug); + await _hubClient.SendEventBatchAsync(_batch.ToList()); + _batch.Clear(); + } + else + { + LogManager.Log($"Queued telemetry message for batching. {_batch.Count}/{BatchSize} currently queued.", LogCategory.Debug); + } + } + else + { + LogManager.Log("Sending single telemetry message to Azure IoT Hub.", LogCategory.Debug); + await _hubClient.SendEventAsync(message); + } + } + + /// + /// Disposes the destination and ensures any remaining batched messages are sent. + /// + public void Dispose() + { + if (_hubClient != null && _batch.Count > 0) + { + LogManager.Log($"Flushing {_batch.Count} remaining messages to Azure IoT Hub.", LogCategory.Info); + _hubClient.SendEventBatchAsync(_batch.ToList()).GetAwaiter().GetResult(); + _batch.Clear(); + } + + _hubClient?.Dispose(); + } + + /// + /// Returns a string that represents the current destination instance. + /// + /// The name of the destination. + public override string ToString() + { + return Name; + } + } +} \ No newline at end of file diff --git a/Software/Visual_Studio/Tango.Telemetry/Destinations/TelemetryMqttDestination.cs b/Software/Visual_Studio/Tango.Telemetry/Destinations/TelemetryMqttDestination.cs new file mode 100644 index 000000000..11787c834 --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/Destinations/TelemetryMqttDestination.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Collections; +using MQTTnet.Client; +using MQTTnet.Client.Options; +using MQTTnet; +using System.Reflection; +using MQTTnet.Client.Connecting; +using Tango.Core; +using MQTTnet.Packets; + +namespace Tango.Telemetry.Destinations +{ + public class TelemetryMqttDestination : ExtendedObject, ITelemetryDestination + { + private IMqttClient _mqttClient; + private IMqttClientOptions _mqttOptions; + private DateTime _nextRealAvailabilityCheck; + + public string Name { get; set; } = "MQTT"; + public String Address { get; private set; } + public int Port { get; private set; } + public String Topic { get; private set; } + + public IReadOnlyList SupportedSourceTypes { get; private set; } + + /// + /// + /// + /// e.g machie/telemetry/serial number + /// Default localhost + /// Default 1883 + public TelemetryMqttDestination(String topic, String address = "localhost", int port = 1883) + { + _nextRealAvailabilityCheck = DateTime.Now; + Topic = topic; + Address = address; + Port = port; + SupportedSourceTypes = new List() { TelemetrySourceTypes.Streaming }; + } + + public async Task IsAvailable() + { + if (_mqttClient == null) + { + return await EnsureConnection(); + } + else + { + if (DateTime.Now > _nextRealAvailabilityCheck) + { + _nextRealAvailabilityCheck = DateTime.Now.AddMinutes(5); + return await EnsureConnection(); + } + else + { + return _mqttClient.IsConnected; + } + } + } + + private async Task EnsureConnection() + { + if (_mqttClient == null || !_mqttClient.IsConnected) + { + try + { + var factory = new MqttFactory(); + _mqttClient = factory.CreateMqttClient(); + + String exeName = Assembly.GetEntryAssembly().GetName().FullName; + _mqttOptions = new MqttClientOptionsBuilder() + .WithClientId(exeName) + .WithTcpServer(Address, Port) + .WithCleanSession() + .Build(); + + var result = await _mqttClient.ConnectAsync(_mqttOptions); + if (result.ResultCode != MqttClientConnectResultCode.Success) + { + LogManager.Log(new Exception($"Error connecting to MQTT broker. {result.ResultCode}")); + return false; + } + } + catch (Exception ex) + { + LogManager.Log(ex, "Error connecting to MQTT broker."); + return false; + } + } + + return true; + } + + public async Task Publish(TelemetryPublishPackage package, List> properties) + { + if (await EnsureConnection()) + { + var message = new MqttApplicationMessageBuilder() + .WithTopic($"{Topic}/{package.PendingTelemetry.TelemetryObject.ToTelemetryName()}") + .WithPayload(package.ToPayload()) + .WithExactlyOnceQoS() + .WithRetainFlag(false) + .Build(); + + foreach (var prop in properties) + { + message.UserProperties.Add(new MqttUserProperty(prop.Key, prop.Value)); + } + + + await _mqttClient.PublishAsync(message); + } + } + + public void Dispose() + { + _mqttClient?.Dispose(); + } + + public override string ToString() + { + return $"{Name} -> {Address}:{Port}"; + } + } +} diff --git a/Software/Visual_Studio/Tango.Telemetry/Docs/AiResponse.md b/Software/Visual_Studio/Tango.Telemetry/Docs/AiResponse.md new file mode 100644 index 000000000..c08e4f5d6 --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/Docs/AiResponse.md @@ -0,0 +1,138 @@ +## ✅ Part 1: Backup and Recovery of LiteDB File + +### 🔧 Strategy + +* **On application startup or shutdown**, copy the main `.db` file to a backup file (e.g., `telemetry.db.bak`). +* **On startup**, if LiteDB is missing or corrupted: + + * Attempt to **restore from backup**. + * If both fail, log a critical failure or initialize an empty one (last resort). + +### 🧱 Suggested Implementation + +```csharp +private const string DbFile = "telemetry.db"; +private const string BackupFile = "telemetry.db.bak"; + +public void EnsureDatabaseIntegrity() +{ + if (!File.Exists(DbFile)) + { + if (File.Exists(BackupFile)) + { + Log.Warning("Main database file missing. Attempting to restore from backup..."); + File.Copy(BackupFile, DbFile); + } + else + { + Log.Critical("No telemetry database or backup found. Initializing fresh DB."); + // Optionally throw or initialize empty + } + } +} + +public void BackupDatabase() +{ + try + { + File.Copy(DbFile, BackupFile, overwrite: true); + Log.Info("Telemetry database backed up successfully."); + } + catch (Exception ex) + { + Log.Error($"Failed to backup telemetry database: {ex}"); + } +} +``` + +Call `BackupDatabase()` on graceful shutdown or every X hours. + +--- + +## ✅ Part 2: Mirror Source Checkpoints to the Cloud + +### 🧠 Why: + +* In PC reinstall or hardware swap scenarios, **LiteDB is lost**, but the machine serial number remains. +* The application can download JobRuns from the server — but without the previous **checkpoints**, it doesn’t know what has already been published. +* Replaying the **entire JobRuns table** results in telemetry flooding. + +--- + +### 🔧 Strategy + +Store a minimal JSON blob to the cloud keyed by **machine serial number**, e.g.: + +```json +{ + "MachineSerial": "X4-3215", + "Checkpoints": { + "TelemetryJobRunsHistorySource": "2025-07-30T14:15:00Z", + "TelemetryEventsHistorySource": "2025-07-30T13:50:00Z" + } +} +``` + +### 📦 Recommended Storage Location + +* Azure Blob Storage +* OR Azure Table / CosmosDB if you already use structured cloud data +* OR SFTP/S3-compatible endpoint if on-prem + +### 💡 Key Design Points + +* Mirror only **checkpoints** (very small, infrequent updates). +* Use **machine serial number as the key**. +* Update cloud **whenever a checkpoint advances** significantly or every X minutes. +* Download cloud checkpoints on boot **only if LiteDB is missing**. + +--- + +### 🧱 Example C# Interface + +```csharp +public interface ICheckpointMirror +{ + Task> DownloadCheckpointsAsync(string machineSerial); + Task UploadCheckpointsAsync(string machineSerial, Dictionary checkpoints); +} +``` + +--- + +## 🧩 Final Integration Flow + +```mermaid +graph TD +A[App Startup] --> B{LiteDB Exists?} +B -- Yes --> C[Continue Normally] +B -- No --> D{Backup Exists?} +D -- Yes --> E[Restore From Backup] +D -- No --> F{Cloud Checkpoints Available?} +F -- Yes --> G[Restore Checkpoints from Cloud] +F -- No --> H[Start From Scratch] + +C --> Z[Load Published GUIDs + Checkpoints] +E --> Z +G --> Z +H --> Z +``` + +--- + +## ✅ Summary + +| Component | Purpose | When to Use | +| ----------------------- | ----------------------------------------------- | ------------------------------- | +| LiteDB Backup | Quick local restore from disk failure | Every boot/shutdown | +| Cloud Checkpoint Mirror | Resilient recovery across reinstalls or new PCs | Only if LiteDB+backup missing | +| Machine Serial Binding | Ensures uniqueness and portability | On X4, X1, or reimaged OS cases | + +--- + +Would you like: + +* A suggested format for the cloud checkpoint JSON file? +* A mock implementation of `ICheckpointMirror` that uploads/downloads to Azure Blob or local disk for testing? + +You're building this with an enterprise-level attention to failure domains. Very smart. diff --git a/Software/Visual_Studio/Tango.Telemetry/ITelemetry.cs b/Software/Visual_Studio/Tango.Telemetry/ITelemetry.cs index 67bb8f5d2..4dc1aa3e6 100644 --- a/Software/Visual_Studio/Tango.Telemetry/ITelemetry.cs +++ b/Software/Visual_Studio/Tango.Telemetry/ITelemetry.cs @@ -10,6 +10,7 @@ namespace Tango.Telemetry { public interface ITelemetry { + String ID { get; set; } DateTime Time { get; set; } String ToJson(Formatting format = Formatting.None, bool flatten = true); byte[] ToBytes(Formatting format = Formatting.None, bool flatten = true); diff --git a/Software/Visual_Studio/Tango.Telemetry/ITelemetryCheckpointsRecoveryClient.cs b/Software/Visual_Studio/Tango.Telemetry/ITelemetryCheckpointsRecoveryClient.cs new file mode 100644 index 000000000..e6cec73bf --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/ITelemetryCheckpointsRecoveryClient.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tango.Telemetry +{ + public interface ITelemetryCheckpointsRecoveryClient : IDisposable + { + Task> GetCheckpointsBackup(); + Task SaveCheckpointsBackup(List checkPoints); + } +} diff --git a/Software/Visual_Studio/Tango.Telemetry/ITelemetryDestination.cs b/Software/Visual_Studio/Tango.Telemetry/ITelemetryDestination.cs index 10424531b..aec3d164a 100644 --- a/Software/Visual_Studio/Tango.Telemetry/ITelemetryDestination.cs +++ b/Software/Visual_Studio/Tango.Telemetry/ITelemetryDestination.cs @@ -6,12 +6,33 @@ using System.Threading.Tasks; namespace Tango.Telemetry { + /// + /// Represents a target destination that can receive and process published telemetry data. + /// Implementations may include cloud services, databases, or custom sinks. + /// public interface ITelemetryDestination : IDisposable { - bool Enable { get; set; } + /// + /// Gets or sets the unique name of the destination used for identification and logging. + /// String Name { get; set; } + + /// + /// Checks whether the destination is currently available to receive telemetry. + /// + /// True if the destination is available; otherwise, false. Task IsAvailable(); + + /// + /// Gets a read-only list of source types that this destination supports. + /// IReadOnlyList SupportedSourceTypes { get; } + + /// + /// Publishes the given telemetry package along with machine metadata properties. + /// + /// The telemetry package to publish. + /// Metadata properties such as SerialNumber, MachineType, and Environment. Task Publish(TelemetryPublishPackage package, List> properties); } -} +} \ No newline at end of file diff --git a/Software/Visual_Studio/Tango.Telemetry/ITelemetryPublisher.cs b/Software/Visual_Studio/Tango.Telemetry/ITelemetryPublisher.cs index b44f567da..9ec7860fe 100644 --- a/Software/Visual_Studio/Tango.Telemetry/ITelemetryPublisher.cs +++ b/Software/Visual_Studio/Tango.Telemetry/ITelemetryPublisher.cs @@ -4,22 +4,103 @@ using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; +using Tango.Telemetry.Reporting; namespace Tango.Telemetry { + /// + /// Defines the interface for a telemetry publisher responsible for managing sources, destinations, + /// storage, and the overall publishing lifecycle. + /// public interface ITelemetryPublisher : IDisposable { + /// + /// Occurs before a telemetry package is published to a destination. + /// event EventHandler PublishingPackage; + + /// + /// Occurs when a telemetry package has been successfully published to a destination. + /// event EventHandler PackagePublished; + + /// + /// Occurs when a telemetry package fails to publish to a destination. + /// event EventHandler PublishPackageFailed; + + /// + /// Occurs when a telemetry publish operation has completed and a publish result is available, + /// indicating the success or failure status for each destination. + /// + event EventHandler PublishResultAvailable; + + /// + /// Gets the storage manager used for telemetry persistence and checkpoint handling. + /// ITelemetryStorageManager StorageManager { get; } + + /// + /// Gets the telemetry queue manager responsible for internal queuing and retry logic. + /// ITelemetryQueueManager QueueManager { get; } + + /// + /// Gets the client used for remote checkpoint recovery. + /// + ITelemetryCheckpointsRecoveryClient CheckpointsRecoveryClient { get; } + + /// + /// Gets the registered telemetry sources. + /// ReadOnlyCollection Sources { get; } + + /// + /// Registers a telemetry source with the publisher. + /// + /// The telemetry source to register. void RegisterSource(ITelemetrySource source); + + /// + /// Gets the registered telemetry destinations. + /// ReadOnlyCollection Destinations { get; } + + /// + /// Registers a telemetry destination with the publisher. + /// + /// The telemetry destination to register. void RegisterDestination(ITelemetryDestination destination); + + /// + /// Gets a value indicating whether the publisher is currently running. + /// bool IsStarted { get; } - void Start(); - void Stop(); + + /// + /// Starts the telemetry publishing pipeline, including sources and destinations. + /// + /// A task representing the asynchronous start operation. + Task Start(); + + /// + /// Stops the telemetry publishing pipeline and releases all resources. + /// + /// A task representing the asynchronous stop operation. + Task Stop(); + + /// + /// Flushes up to the specified number of pending telemetries from local storage, + /// attempting to publish them immediately. This can be used to force a retry of previously failed or postponed telemetry packages. + /// + /// The maximum number of pending telemetry packages to flush. + /// A task that represents the asynchronous flush operation, returning a list of publish results for the flushed packages. + Task> FlushPendingTelemetries(int maxCount); + + /// + /// Generates a detailed telemetry report summarizing the current state of the telemetry system. + /// The report includes statistics on published and pending telemetry, as well as per-source and per-destination results. + /// + Task GetTelemetryReport(); } } diff --git a/Software/Visual_Studio/Tango.Telemetry/ITelemetrySource.cs b/Software/Visual_Studio/Tango.Telemetry/ITelemetrySource.cs index 74d58ed4a..739c6514b 100644 --- a/Software/Visual_Studio/Tango.Telemetry/ITelemetrySource.cs +++ b/Software/Visual_Studio/Tango.Telemetry/ITelemetrySource.cs @@ -9,5 +9,6 @@ namespace Tango.Telemetry public interface ITelemetrySource : IDisposable { String Name { get; } + bool RequiresTelemetryDuplicationTracking { get; } } } diff --git a/Software/Visual_Studio/Tango.Telemetry/ITelemetryStorageManager.cs b/Software/Visual_Studio/Tango.Telemetry/ITelemetryStorageManager.cs index ae63e41cd..882cc0411 100644 --- a/Software/Visual_Studio/Tango.Telemetry/ITelemetryStorageManager.cs +++ b/Software/Visual_Studio/Tango.Telemetry/ITelemetryStorageManager.cs @@ -6,13 +6,85 @@ using System.Threading.Tasks; namespace Tango.Telemetry { + /// + /// Defines the contract for managing telemetry storage, including pending telemetries and history source checkpoints. + /// public interface ITelemetryStorageManager { + /// + /// Initializes the storage manager with the specified checkpoints recovery client. + /// Responsible for loading the database, restoring from backup if necessary, and recovering remote checkpoints. + /// + /// An implementation of the checkpoint recovery client used for cloud fallback. + Task Init(ITelemetryCheckpointsRecoveryClient checkpointsRecoveryClient); + + /// + /// Inserts or updates a pending telemetry record in the local storage. + /// + /// The pending telemetry to be stored or updated. void UpsertPendingTelemetry(PendingTelemetry pendingTelemetry); + + /// + /// Deletes a pending telemetry record from the local storage. + /// + /// The pending telemetry to be deleted. void DeletePendingTelemetry(PendingTelemetry pendingTelemetry); + + /// + /// Retrieves a list of pending telemetry records, ordered by time, up to the specified maximum count. + /// + /// The maximum number of pending telemetries to retrieve. + /// A list of pending telemetry objects. List GetPendingTelemetries(int maxCount); + + /// + /// Gets the total number of pending telemetry records currently stored. + /// + /// The count of pending telemetry records. int GetPendingTelemetriesCount(); + + /// + /// Retrieves the current checkpoint for the specified history source. + /// + /// The telemetry history source for which to retrieve the checkpoint. + /// The stored checkpoint information for the given source. TelemetryHistorySourceCheckPoint GetHistorySourceCheckPoint(ITelemetryHistorySource source); + + /// + /// Retrieves all stored history source checkpoints currently tracked by the storage system. + /// Each checkpoint represents the latest processed state of a specific telemetry history source. + /// + /// A list of entries for all registered history sources. + List GetHistorySourcesCheckPoints(); + + /// + /// Sets or updates the checkpoint for the specified history source. + /// + /// The telemetry history source for which to update the checkpoint. + /// The latest timestamp of telemetry data processed for the source. + /// The total number of telemetry records processed for the source. void SetHistorySourceCheckPoint(ITelemetryHistorySource source, DateTime time, int totalCount); + + /// + /// Adds the specified telemetry item to the published telemetry cache, + /// ensuring it is tracked as already published by the system. + /// + /// The telemetry instance to register as published. + void AddToPublishedTelemetryCache(ITelemetry telemetry); + + /// + /// Checks whether the specified telemetry item is already present in the published telemetry cache. + /// + /// The telemetry instance to verify. + /// True if the telemetry appears to have already been published; otherwise, false. + bool IsTelemetryInPublishedCache(ITelemetry telementry); + + /// + /// Removes entries from the published telemetry cache that were marked as published + /// before the specified time. Intended to manage memory and storage growth over time, + /// especially once historical sources have progressed beyond the given point. + /// + /// The timestamp indicating the oldest publication time to retain. Entries older than this will be removed. + void PerformPublishedTelemetriesCleanUp(DateTime olderThan); } } diff --git a/Software/Visual_Studio/Tango.Telemetry/Reporting/DestinationStatusSummary.cs b/Software/Visual_Studio/Tango.Telemetry/Reporting/DestinationStatusSummary.cs new file mode 100644 index 000000000..a8dc52123 --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/Reporting/DestinationStatusSummary.cs @@ -0,0 +1,36 @@ +namespace Tango.Telemetry.Reporting +{ + /// + /// Represents an aggregated summary of telemetry publish results for a specific destination, + /// tracking the number of successful, failed, postponed, and unavailable attempts. + /// + public class DestinationStatusSummary + { + /// + /// Gets or sets the name of the telemetry destination (e.g., IoTHub, LocalStorage). + /// + public string DestinationName { get; set; } + + /// + /// Gets or sets the number of telemetry packages successfully published to this destination. + /// + public int Passed { get; set; } + + /// + /// Gets or sets the number of telemetry packages that failed to publish to this destination. + /// + public int Failed { get; set; } + + /// + /// Gets or sets the number of telemetry packages that were postponed for this destination, + /// typically due to retry logic or temporary conditions. + /// + public int Postponed { get; set; } + + /// + /// Gets or sets the number of telemetry packages that could not be published due to the destination being unavailable. + /// + public int Unavailable { get; set; } + } + +} diff --git a/Software/Visual_Studio/Tango.Telemetry/Reporting/SourceSummary.cs b/Software/Visual_Studio/Tango.Telemetry/Reporting/SourceSummary.cs new file mode 100644 index 000000000..04e57e985 --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/Reporting/SourceSummary.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace Tango.Telemetry.Reporting +{ + /// + /// Represents a summary of telemetry publish results for a specific telemetry source, + /// including aggregated destination-level status counts. + /// + public class SourceSummary + { + /// + /// Gets or sets the name of the telemetry source (e.g., JobRunsHistorySource, DiagnosticsStream). + /// + public string SourceName { get; set; } + + /// + /// Gets or sets a dictionary of aggregated publish results per destination for this source. + /// The key is the destination name, and the value contains counters for each delivery status. + /// + public Dictionary Destinations { get; set; } + + /// + /// Initializes a new instance of the class, + /// initializing the destination summary dictionary. + /// + public SourceSummary() + { + Destinations = new Dictionary(); + } + } +} diff --git a/Software/Visual_Studio/Tango.Telemetry/Reporting/SourceTypeSummary.cs b/Software/Visual_Studio/Tango.Telemetry/Reporting/SourceTypeSummary.cs new file mode 100644 index 000000000..9b6dc05d9 --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/Reporting/SourceTypeSummary.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; + +namespace Tango.Telemetry.Reporting +{ + /// + /// Represents an aggregated summary of telemetry sources belonging to a specific source type, + /// such as Streaming, ExternalStorage, or PendingStorage. + /// Groups multiple sources under the same category and aggregates their publish results. + /// + public class SourceTypeSummary + { + /// + /// Gets or sets the telemetry source type that this summary represents. + /// + public TelemetrySourceTypes SourceType { get; set; } + + /// + /// Gets or sets a dictionary of source summaries under this source type. + /// The key is the source name, and the value is a summary of its destination-level results. + /// + public Dictionary Sources { get; set; } + + /// + /// Initializes a new instance of the class, + /// initializing the source summary dictionary. + /// + public SourceTypeSummary() + { + Sources = new Dictionary(); + } + } + +} diff --git a/Software/Visual_Studio/Tango.Telemetry/Reporting/TelemetryReport.cs b/Software/Visual_Studio/Tango.Telemetry/Reporting/TelemetryReport.cs new file mode 100644 index 000000000..9e9906fde --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/Reporting/TelemetryReport.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tango.Telemetry.Reporting +{ + /// + /// Represents a diagnostic snapshot of the telemetry system, + /// providing detailed statistics about published and pending telemetry data, + /// grouped by source type and destination. + /// + public class TelemetryReport + { + /// + /// Gets or sets the timestamp indicating when the report was generated. + /// + public DateTime GeneratedAt { get; set; } + + /// + /// Gets or sets the total number of telemetry packages that were published + /// successfully since the telemetry system started. + /// + public int TotalPublished { get; set; } + + /// + /// Gets or sets the current number of telemetry packages pending publication in storage. + /// + public int TotalPending { get; set; } + + /// + /// Gets or sets the aggregated telemetry publish summaries, grouped by source type. + /// Each source type contains its respective sources and destination-level statistics. + /// + public Dictionary SourceTypes { get; set; } + + /// + /// Initializes a new instance of the class, + /// initializing the dictionary used for grouping by source type. + /// + public TelemetryReport() + { + SourceTypes = new Dictionary(); + } + + /// + /// Returns a human-readable string representation of the report, + /// including summary statistics and breakdowns by source type and destination. + /// + /// A formatted string representing the current telemetry report. + public override string ToString() + { + var sb = new StringBuilder(); + + sb.AppendLine("===== Telemetry Report ====="); + sb.AppendLine($"Generated At : {GeneratedAt:u}"); + sb.AppendLine($"Total Published : {TotalPublished}"); + sb.AppendLine($"Total Pending : {TotalPending}"); + sb.AppendLine(); + + foreach (var sourceTypePair in SourceTypes) + { + sb.AppendLine($"--- Source Type: {sourceTypePair.Key} ---"); + + foreach (var sourcePair in sourceTypePair.Value.Sources) + { + sb.AppendLine($" Source: {sourcePair.Key}"); + + foreach (var destinationPair in sourcePair.Value.Destinations) + { + var d = destinationPair.Value; + sb.AppendLine($" {d.DestinationName}: Passed={d.Passed}, Failed={d.Failed}, Postponed={d.Postponed}, Unavailable={d.Unavailable}"); + } + + sb.AppendLine(); // spacing between sources + } + + sb.AppendLine(); // spacing between source types + } + + sb.AppendLine("============================"); + return sb.ToString(); + } + } +} diff --git a/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryDiagnosticsSource.cs b/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryDiagnosticsSource.cs index 22fac087b..5a2735131 100644 --- a/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryDiagnosticsSource.cs +++ b/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryDiagnosticsSource.cs @@ -14,7 +14,7 @@ using Tango.Telemetry.Telemetries; namespace Tango.Telemetry.Sources { - public class TelemetryDiagnosticsSource : TelemetryConfigurableSource, ITelemetrySource + public class TelemetryDiagnosticsSource : TelemetryConfigurableSource, ITelemetryStreamingSource { public const int MIN_SAMPLING_INTERVAL_SECONDS = 1; @@ -30,6 +30,8 @@ namespace Tango.Telemetry.Sources public string Name { get; private set; } = "Diagnostics"; + public bool RequiresTelemetryDuplicationTracking { get => false; } + private TelemetryDiagnosticsSource() : base() { _diagnosticsQueue = new List(); diff --git a/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryJobRunsHistoryModule.cs b/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryJobRunsHistoryModule.cs deleted file mode 100644 index 6cb597e4b..000000000 --- a/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryJobRunsHistoryModule.cs +++ /dev/null @@ -1,61 +0,0 @@ -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.Telemetries; - -namespace Tango.Telemetry.Sources -{ - public class TelemetryJobRunsHistorySource : TelemetryConfigurableSource, ITelemetryHistorySource - { - private bool _isBusy; - - public string Name { get; private set; } = "JobRuns History"; - - public Task CanRequestHistory(DateTime from) - { - return Task.FromResult(!_isBusy); - } - - public async Task> RequestHistory(DateTime from) - { - try - { - _isBusy = true; - - using (ObservablesContext db = ObservablesContext.CreateDefault()) - { - var runs = await db.JobRuns - .Where(x => x.LastUpdated > from) - .OrderBy(x => x.LastUpdated) - .Take(Config.MaxJobRunsPerRequest) - .ToListAsync(); - - List tRuns = new List(); - - foreach (var run in runs) - { - TelemetryJobRun tRun = new TelemetryJobRun(); - tRun.Time = run.LastUpdated; - //Fill the object.. - tRuns.Add(tRun); - } - - return tRuns; - } - } - finally - { - _isBusy = false; - } - } - - public void Dispose() - { - - } - } -} diff --git a/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryJobRunsHistorySource.cs b/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryJobRunsHistorySource.cs new file mode 100644 index 000000000..e3934d832 --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/Sources/TelemetryJobRunsHistorySource.cs @@ -0,0 +1,65 @@ +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.Telemetries; + +namespace Tango.Telemetry.Sources +{ + public class TelemetryJobRunsHistorySource : TelemetryConfigurableSource, ITelemetryHistorySource + { + private bool _isBusy; + + public string Name { get; private set; } = "JobRuns History"; + + public bool RequiresTelemetryDuplicationTracking { get => true; } + + public Task CanRequestHistory(DateTime from) + { + return Task.FromResult(!_isBusy); + } + + public async Task> RequestHistory(DateTime from) + { + try + { + _isBusy = true; + + using (ObservablesContext db = ObservablesContext.CreateDefault()) + { + var runs = await db.JobRuns + .Where(x => x.LastUpdated > from) + .OrderBy(x => x.LastUpdated) + .Take(Config.MaxJobRunsPerRequest) + .ToListAsync(); + + List tRuns = new List(); + + foreach (var run in runs) + { + TelemetryJobRun tRun = new TelemetryJobRun(); + tRun.ID = run.Guid; + tRun.Time = run.LastUpdated; + tRun.Name = run.JobName; + //Fill the object.. + tRuns.Add(tRun); + } + + return tRuns; + } + } + finally + { + _isBusy = false; + } + } + + public void Dispose() + { + + } + } +} diff --git a/Software/Visual_Studio/Tango.Telemetry/Tango.Telemetry.csproj b/Software/Visual_Studio/Tango.Telemetry/Tango.Telemetry.csproj index 02f8417f1..9565465a5 100644 --- a/Software/Visual_Studio/Tango.Telemetry/Tango.Telemetry.csproj +++ b/Software/Visual_Studio/Tango.Telemetry/Tango.Telemetry.csproj @@ -61,15 +61,24 @@ ..\packages\Microsoft.Azure.Amqp.2.5.10\lib\net45\Microsoft.Azure.Amqp.dll - - ..\packages\Microsoft.Azure.Devices.Client.1.41.0\lib\net451\Microsoft.Azure.Devices.Client.dll + + ..\packages\Microsoft.Azure.Devices.Client.1.6.0\lib\net45\Microsoft.Azure.Devices.Client.dll - - ..\packages\Microsoft.Azure.Devices.Shared.1.30.1\lib\net451\Microsoft.Azure.Devices.Shared.dll + + ..\packages\Microsoft.Azure.Devices.Shared.1.3.0\lib\net45\Microsoft.Azure.Devices.Shared.dll ..\packages\Microsoft.Azure.KeyVault.Core.1.0.0\lib\net40\Microsoft.Azure.KeyVault.Core.dll + + ..\packages\Microsoft.Data.Edm.5.8.2\lib\net40\Microsoft.Data.Edm.dll + + + ..\packages\Microsoft.Data.OData.5.8.2\lib\net40\Microsoft.Data.OData.dll + + + ..\packages\Microsoft.Data.Services.Client.5.8.2\lib\net40\Microsoft.Data.Services.Client.dll + ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.1.1.0\lib\netstandard1.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll @@ -82,21 +91,39 @@ ..\packages\Microsoft.Owin.4.0.0\lib\net451\Microsoft.Owin.dll + + ..\packages\EnterpriseLibrary.TransientFaultHandling.6.0.1304.0\lib\portable-net45+win+wp8\Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling.dll + ..\packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll - - ..\packages\WindowsAzure.Storage.9.3.2\lib\net45\Microsoft.WindowsAzure.Storage.dll + + ..\packages\WindowsAzure.Storage.8.7.0\lib\net45\Microsoft.WindowsAzure.Storage.dll ..\packages\MQTTnet.3.1.2\lib\net461\MQTTnet.dll - - ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll + + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll ..\packages\Owin.1.0\lib\net40\Owin.dll + + ..\packages\PCLCrypto.2.0.147\lib\net45\PCLCrypto.dll + + + ..\packages\PInvoke.BCrypt.0.3.2\lib\net40\PInvoke.BCrypt.dll + + + ..\packages\PInvoke.Kernel32.0.3.2\lib\net40\PInvoke.Kernel32.dll + + + ..\packages\PInvoke.NCrypt.0.3.2\lib\net40\PInvoke.NCrypt.dll + + + ..\packages\PInvoke.Windows.Core.0.3.2\lib\portable-net45+win+wpa81+MonoAndroid10+xamarinios10+MonoTouch10\PInvoke.Windows.Core.dll + ..\packages\System.AppContext.4.3.0\lib\net46\System.AppContext.dll @@ -186,6 +213,9 @@ True True + + ..\packages\System.Spatial.5.8.2\lib\net40\System.Spatial.dll + ..\packages\System.Threading.Tasks.Extensions.4.5.1\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll @@ -201,21 +231,29 @@ True True + + ..\packages\Validation.2.2.8\lib\dotnet\Validation.dll + - + + - + + + + + - + @@ -227,13 +265,15 @@ - + + + @@ -244,6 +284,7 @@ + diff --git a/Software/Visual_Studio/Tango.Telemetry/Telemetries/TelemetryJobRun.cs b/Software/Visual_Studio/Tango.Telemetry/Telemetries/TelemetryJobRun.cs index 7b1ca3a1c..5aca89ee0 100644 --- a/Software/Visual_Studio/Tango.Telemetry/Telemetries/TelemetryJobRun.cs +++ b/Software/Visual_Studio/Tango.Telemetry/Telemetries/TelemetryJobRun.cs @@ -9,6 +9,29 @@ namespace Tango.Telemetry.Telemetries [TelemetryName("JobRun")] public class TelemetryJobRun : TelemetryBase { + public String Name { get; set; } + public String JobKind { get; set; } + public String Thread { get; set; } + public String NumberOfUnits { get; set; } + + public double LogicalLength { get; set; } + public double ActualLength { get; set; } + public double StartPosition { get; set; } + public double EndPosition { get; set; } + public double Distance { get; set; } + + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public TimeSpan Duration { get; set; } + + public String Status { get; set; } + + public String ColorSpace { get; set; } + + public double TargetL { get; set; } + public double TargetA { get; set; } + public double TargetB { get; set; } + } } diff --git a/Software/Visual_Studio/Tango.Telemetry/TelemetryBase.cs b/Software/Visual_Studio/Tango.Telemetry/TelemetryBase.cs index 20d5211e7..c2879e9db 100644 --- a/Software/Visual_Studio/Tango.Telemetry/TelemetryBase.cs +++ b/Software/Visual_Studio/Tango.Telemetry/TelemetryBase.cs @@ -10,14 +10,22 @@ namespace Tango.Telemetry { public class TelemetryBase : ITelemetry { + public string ID { get; set; } + + public DateTime Time { get; set; } + + public TelemetryBase() + { + ID = Guid.NewGuid().ToString(); + Time = DateTime.UtcNow; + } + [BsonIgnore] //This will be used for column mapping in ADX public String Type { get { return this.ToTelemetryName(); } } - public DateTime Time { get; set; } - public byte[] ToBytes(Formatting format = Formatting.None, bool flatten = true) { return Encoding.UTF8.GetBytes(ToJson(format, flatten)); diff --git a/Software/Visual_Studio/Tango.Telemetry/TelemetryLiteDBStorageManager.cs b/Software/Visual_Studio/Tango.Telemetry/TelemetryLiteDBStorageManager.cs index 2ceb95298..6700ba8af 100644 --- a/Software/Visual_Studio/Tango.Telemetry/TelemetryLiteDBStorageManager.cs +++ b/Software/Visual_Studio/Tango.Telemetry/TelemetryLiteDBStorageManager.cs @@ -5,45 +5,131 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using Tango.Core; +using Tango.Core.ExtensionMethods; +using Tango.Logging; namespace Tango.Telemetry { - public class TelemetryLiteDBStorageManager : ITelemetryStorageManager + public class TelemetryLiteDBStorageManager : ExtendedObject, ITelemetryStorageManager { + public class PublishedTelemetry + { + public String ID { get; set; } + public DateTime CreatedAt { get; set; } + } + private bool _disposed; private LiteDatabase _database; private static Object _lock = new object(); + private ITelemetryCheckpointsRecoveryClient _checkpointsRecoveryClient; + private HashSet _publishedTelemetriesIDs; + private DateTime _lastCloudBackupTime = DateTime.MinValue; + private TimeSpan _checkpointsBackupInterval = TimeSpan.FromMinutes(1); + + public TimeSpan CheckpointsBackupInterval { get => _checkpointsBackupInterval; set => _checkpointsBackupInterval = value >= TimeSpan.FromMinutes(1) ? value : TimeSpan.FromMinutes(1); } public String DatabasePath { get; private set; } + public bool EnableCheckPointsRecovery { get; set; } + public bool EnforceCheckpointsRecovery { get; set; } + public TelemetryLiteDBStorageManager() { + _publishedTelemetriesIDs = new HashSet(); + EnableCheckPointsRecovery = true; + EnforceCheckpointsRecovery = true; DatabasePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Twine", "Tango", "Telemetry", Path.GetFileNameWithoutExtension(AppDomain.CurrentDomain.FriendlyName) + ".telemetry"); + } + + public TelemetryLiteDBStorageManager(String databaseFile) : this() + { + DatabasePath = databaseFile; + } + + public async Task Init(ITelemetryCheckpointsRecoveryClient checkpointsRecoveryClient) + { + LogManager.Log("Initializing telemetry database..."); + + _checkpointsRecoveryClient = checkpointsRecoveryClient; + Directory.CreateDirectory(Path.GetDirectoryName(DatabasePath)); + String backupPath = DatabasePath + ".bak"; + + if (!File.Exists(DatabasePath)) + { + if (File.Exists(backupPath)) + { + LogManager.Log("Telemetry database missing. Attempting to restore from backup.", LogCategory.Error); + File.Copy(backupPath, DatabasePath, overwrite: true); + } + else + { + LogManager.Log("Telemetry database was not found. A new one will be created and source checkpoints will be recovered from the remote service if required.", LogCategory.Critical); + + if (_checkpointsRecoveryClient == null && EnableCheckPointsRecovery && EnforceCheckpointsRecovery) + { + throw new NullReferenceException("No TelemetryCheckpointsRecoveryClient was introduced. Telemetry Storage manager should not operate."); + } + } + } + _database = new LiteDatabase($"Filename={DatabasePath}"); _database.Pragma("TIMEOUT", 10); //Read Timeout _database.Pragma("UTC_DATE", true); //Keep time as UTC when getting data _database.Commit(); - } - public virtual void Dispose() - { - if (_database != null && !_disposed) + var checkPointsCollection = GetSourcesCheckpointCollection(); + var localCheckPoints = checkPointsCollection.FindAll().ToList(); + + if (localCheckPoints.Count == 0) { - try + if (EnableCheckPointsRecovery) { - _disposed = true; - _database.Dispose(); - _database = null; + try + { + LogManager.Log("Attempting to retrieve sources checkpoints from backup..."); + var remoteCheckPoints = await _checkpointsRecoveryClient.GetCheckpointsBackup(); + if (remoteCheckPoints.Count > 0) + { + checkPointsCollection.InsertBulk(remoteCheckPoints); + LogManager.Log($"Sources checkpoints successfully recovered.\n{remoteCheckPoints.ToJsonString()}"); + } + else + { + LogManager.Log("No sources checkpoint found on backup. Assuming first operation..."); + } + } + catch (Exception ex) + { + if (EnforceCheckpointsRecovery) + { + LogManager.Log(ex, LogCategory.Critical, "Could not retrieve sources checkpoints from backup. Telemetry storage manager should not operate."); + throw; + } + else + { + LogManager.Log(ex, LogCategory.Warning, "Could not retrieve sources checkpoints from backup. No Checkpoints available!"); + } + } } - catch { } } + else + { + var minDateTime = localCheckPoints.Min(x => x.Time); + PerformPublishedTelemetriesCleanUp(minDateTime); + } + + LogManager.Log("Loading published telemetries cache..."); + _publishedTelemetriesIDs = new HashSet(GetPublishedTelemetriesCollection().FindAll().Select(x => x.ID).ToList()); + + LogManager.Log("Telemetry LiteDB storage manager initialized..."); } - ~TelemetryLiteDBStorageManager() + private ILiteCollection GetPublishedTelemetriesCollection() { - Dispose(); + return _database.GetCollection("PublishedTelemetries"); } private ILiteCollection GetPendingTelemetriesCollection() @@ -79,7 +165,8 @@ namespace Tango.Telemetry lock (_lock) { var collection = GetPendingTelemetriesCollection(); - return collection.FindAll().OrderBy(x => x.TelemetryObject.Time).Take(Math.Max(maxCount, 1)).ToList(); + var pendingTelemetries = collection.FindAll().OrderBy(x => x.TelemetryObject.Time).Take(Math.Max(maxCount, 1)).ToList(); + return pendingTelemetries; } } @@ -88,7 +175,26 @@ namespace Tango.Telemetry lock (_lock) { var collection = GetSourcesCheckpointCollection(); - return collection.FindOne(x => x.SourceName == source.Name); + var checkpoint = collection.FindOne(x => x.SourceName == source.Name); + + if (checkpoint == null) + { + checkpoint = new TelemetryHistorySourceCheckPoint(); + checkpoint.SourceName = source.Name; + checkpoint.Time = DateTime.MinValue; + } + + return checkpoint; + } + } + + public List GetHistorySourcesCheckPoints() + { + lock (_lock) + { + var collection = GetSourcesCheckpointCollection(); + var checkpoints = collection.FindAll().ToList(); + return checkpoints; } } @@ -98,6 +204,24 @@ namespace Tango.Telemetry { var collection = GetSourcesCheckpointCollection(); collection.Upsert(new TelemetryHistorySourceCheckPoint() { SourceName = source.Name, Time = time, TotalCount = totalCount }); + + if (_checkpointsRecoveryClient != null && DateTime.UtcNow - _lastCloudBackupTime > CheckpointsBackupInterval) + { + _lastCloudBackupTime = DateTime.UtcNow; + Task.Run(async () => + { + try + { + var allCheckpoints = collection.FindAll().ToList(); + await _checkpointsRecoveryClient.SaveCheckpointsBackup(allCheckpoints); + LogManager.Log("Sources checkpoints successfully backed up to remote service."); + } + catch (Exception ex) + { + LogManager.Log(ex, LogCategory.Warning, "Failed to back up checkpoints to remote service."); + } + }); + } } } @@ -106,8 +230,47 @@ namespace Tango.Telemetry lock (_lock) { var collection = GetPendingTelemetriesCollection(); - return collection.Count(); + var count = collection.Count(); + return count; + } + } + + public void AddToPublishedTelemetryCache(ITelemetry telemetry) + { + _publishedTelemetriesIDs.Add(telemetry.ID); + GetPublishedTelemetriesCollection().Insert(new PublishedTelemetry() { CreatedAt = DateTime.UtcNow, ID = telemetry.ID }); + } + + public bool IsTelemetryInPublishedCache(ITelemetry telementry) + { + return _publishedTelemetriesIDs.Contains(telementry.ID); + } + + public void PerformPublishedTelemetriesCleanUp(DateTime olderThan) + { + LogManager.Log("Performing published telemetries cache cleanup..."); + var collection = GetSourcesCheckpointCollection(); + int deleted = collection.DeleteMany(x => x.Time < olderThan); + LogManager.Log($"Published telemetries cleanup completed. {deleted} cleaned."); + } + + public virtual void Dispose() + { + if (_database != null && !_disposed) + { + try + { + _disposed = true; + _database.Dispose(); + _database = null; + } + catch { } } } + + ~TelemetryLiteDBStorageManager() + { + Dispose(); + } } } diff --git a/Software/Visual_Studio/Tango.Telemetry/TelemetryPendingStorageSource.cs b/Software/Visual_Studio/Tango.Telemetry/TelemetryPendingStorageSource.cs index 6aa1f5527..a5e176ca7 100644 --- a/Software/Visual_Studio/Tango.Telemetry/TelemetryPendingStorageSource.cs +++ b/Software/Visual_Studio/Tango.Telemetry/TelemetryPendingStorageSource.cs @@ -9,6 +9,7 @@ namespace Tango.Telemetry public class TelemetryPendingStorageSource : ITelemetrySource { public string Name { get; private set; } = "Pending Storage"; + public bool RequiresTelemetryDuplicationTracking { get => false; } public void Dispose() { diff --git a/Software/Visual_Studio/Tango.Telemetry/TelemetryPublishPackage.cs b/Software/Visual_Studio/Tango.Telemetry/TelemetryPublishPackage.cs index baafb70a7..7b6b577a0 100644 --- a/Software/Visual_Studio/Tango.Telemetry/TelemetryPublishPackage.cs +++ b/Software/Visual_Studio/Tango.Telemetry/TelemetryPublishPackage.cs @@ -1,4 +1,5 @@ -using System; +using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -10,15 +11,26 @@ namespace Tango.Telemetry { private String _payload; + internal TaskCompletionSource CompletionSource { get; set; } public ITelemetrySource Source { get; set; } public PendingTelemetry PendingTelemetry { get; set; } public TelemetrySourceTypes SourceType { get; set; } - public String ToPayload() + public String Environment { get; set; } + public String SerialNumber { get; set; } + public String MachineType { get; set; } + public String TelemetryName { get; internal set; } + + public TelemetryPublishPackage() + { + CompletionSource = new TaskCompletionSource(); + } + + public String ToPayload(Formatting format = Formatting.None, bool flatten = false) { if (_payload == null) { - _payload = PendingTelemetry.TelemetryObject.ToJson(Newtonsoft.Json.Formatting.None, flatten: true); + _payload = PendingTelemetry.TelemetryObject.ToJson(format, flatten); } return _payload; diff --git a/Software/Visual_Studio/Tango.Telemetry/TelemetryPublishResult.cs b/Software/Visual_Studio/Tango.Telemetry/TelemetryPublishResult.cs index 37c6af412..b423580d2 100644 --- a/Software/Visual_Studio/Tango.Telemetry/TelemetryPublishResult.cs +++ b/Software/Visual_Studio/Tango.Telemetry/TelemetryPublishResult.cs @@ -6,8 +6,15 @@ using System.Threading.Tasks; namespace Tango.Telemetry { + /// + /// Represents the result of publishing a telemetry package to one or more destinations. + /// Contains the status and metrics per destination and total publish time. + /// public class TelemetryPublishResult { + /// + /// Defines the outcome of an attempt to publish to a specific destination. + /// public enum DestinationStatus { None, @@ -16,26 +23,98 @@ namespace Tango.Telemetry Failed, Postponed } - + + /// + /// Contains information about the result of publishing telemetry to a specific destination. + /// public class DestinationResult { + /// + /// The destination to which the telemetry was attempted to be published. + /// public ITelemetryDestination Destination { get; set; } + + /// + /// The result status of the publish attempt. + /// public DestinationStatus Status { get; set; } + + /// + /// Any error that occurred during the publish attempt. + /// public Exception Error { get; set; } + + /// + /// The amount of time it took to attempt publishing to this destination. + /// public TimeSpan ElapsedTime { get; set; } + + /// + /// Number of retry attempts for this destination. + /// + public int RetryCount { get; internal set; } + + /// + /// Time until the next eligible retry attempt. + /// + public TimeSpan RetryDelay { get; internal set; } } + /// + /// Gets or sets the telemetry source that generated the package associated with this publish result. + /// + public ITelemetrySource Source { get; set; } + + /// + /// Gets or sets the source type of the telemetry (e.g., Streaming, ExternalStorage, PendingStorage). + /// + public TelemetrySourceTypes SourceType { get; set; } + + /// + /// List of results for each destination that was part of the publish process. + /// public List DestinationsResults { get; set; } + + /// + /// Total elapsed time taken to publish the telemetry package across all destinations. + /// public TimeSpan TotalElapsedTime { get; set; } + /// + /// Time spent outside of destination publishing, typically system overhead or coordination. + /// public TimeSpan OverheadTime { get { return TimeSpan.FromMilliseconds(TotalElapsedTime.TotalMilliseconds - DestinationsResults.Sum(x => x.ElapsedTime.TotalMilliseconds)); } } + /// + /// Initializes a new instance of the TelemetryPublishResult class. + /// public TelemetryPublishResult() { DestinationsResults = new List(); } + + public override string ToString() + { + var sb = new StringBuilder(); + + sb.AppendLine($"Source: {Source?.Name ?? "Unknown"} ({SourceType})"); + sb.AppendLine($"Total Elapsed Time: {TotalElapsedTime.TotalMilliseconds:F1} ms"); + sb.AppendLine($"Overhead Time: {OverheadTime.TotalMilliseconds:F1} ms"); + sb.AppendLine("Destination Results:"); + + foreach (var result in DestinationsResults ?? Enumerable.Empty()) + { + sb.Append($" - {result.Destination.Name}: {result.Status}, {result.ElapsedTime.TotalMilliseconds:F1} ms"); + if (!string.IsNullOrWhiteSpace(result.Error.ToStringSafe())) + sb.Append($" (Error: {result.Error})"); + + sb.AppendLine(); + } + + return sb.ToString(); + } } } diff --git a/Software/Visual_Studio/Tango.Telemetry/TelemetryPublishResultAvailableEventArgs.cs b/Software/Visual_Studio/Tango.Telemetry/TelemetryPublishResultAvailableEventArgs.cs new file mode 100644 index 000000000..2a177d2ab --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/TelemetryPublishResultAvailableEventArgs.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tango.Telemetry +{ + public class TelemetryPublishResultAvailableEventArgs : TelemetryPublisherEventArgs + { + public TelemetryPublishResult PublishResult { get; set; } + } +} diff --git a/Software/Visual_Studio/Tango.Telemetry/TelemetryPublisher.cs b/Software/Visual_Studio/Tango.Telemetry/TelemetryPublisher.cs index 99d96edff..42121d884 100644 --- a/Software/Visual_Studio/Tango.Telemetry/TelemetryPublisher.cs +++ b/Software/Visual_Studio/Tango.Telemetry/TelemetryPublisher.cs @@ -16,45 +16,109 @@ using Tango.Integration.Operation; using Tango.Logging; using Tango.PMR.Diagnostics; using Tango.PMR.Insights; +using Tango.Telemetry.Reporting; using Tango.Telemetry.Telemetries; namespace Tango.Telemetry { + /// + /// TelemetryPublisher is responsible for collecting telemetry data from sources, + /// queuing it, storing it if needed, and publishing it to one or more destinations. + /// It supports streaming, historical, and retry-based telemetry flows. + /// public class TelemetryPublisher : ExtendedObject, ITelemetryPublisher { + /// + /// Occurs before a telemetry package is published to a destination. + /// public event EventHandler PublishingPackage; + + /// + /// Occurs when a telemetry package has been successfully published to a destination. + /// public event EventHandler PackagePublished; + + /// + /// Occurs when a telemetry package fails to publish to a destination. + /// public event EventHandler PublishPackageFailed; + /// + /// Occurs when a telemetry publish operation has completed and a publish result is available, + /// indicating the success or failure status for each destination. + /// + public event EventHandler PublishResultAvailable; + + // Timer to periodically check and publish pending telemetry from local storage private System.Timers.Timer _pendingStorageCheckTimer; + + // Timer to periodically fetch historical data from ITelemetryHistorySource private System.Timers.Timer _historicalDataTimer; + // Indicates if the publisher has been disposed protected bool _isDisposed; + + // Background thread responsible for dequeuing and publishing telemetry private Thread _publishThread; + + // Source used to tag telemetry loaded from pending storage private TelemetryPendingStorageSource _pendingStorageSource; + //Timer responsible for triggering periodic cleanup of the published telemetries cache, + private System.Timers.Timer _publishedTelemetriesCacheCleanupTimer; + + private List _pastResults; + #region Properties + /// + /// Indicates whether the publisher is actively running. + /// public bool IsStarted { get; private set; } - public TelemetryPublisherConfiguration Config { get; private set; } + /// + /// Publisher configuration containing telemetry parameters and limits. + /// + public TelemetryPublisherConfiguration Config { get; } - public ITelemetryStorageManager StorageManager { get; private set; } + /// + /// Manages persistence of telemetry data (e.g., LiteDB). + /// + public ITelemetryStorageManager StorageManager { get; } private List InnerSources { get; } + /// + /// Public read-only access to telemetry sources. + /// public ReadOnlyCollection Sources { get; } private List InnerDestinations { get; } + /// + /// Public read-only access to telemetry destinations. + /// public ReadOnlyCollection Destinations { get; } + /// + /// Manages telemetry queuing between ingestion and publish phases. + /// public ITelemetryQueueManager QueueManager { get; private set; } + /// + /// Gets the client used for remote checkpoint recovery. + /// + public ITelemetryCheckpointsRecoveryClient CheckpointsRecoveryClient { get; } + #endregion #region Constructor - public TelemetryPublisher(TelemetryPublisherConfiguration config) + /// + /// Initializes the telemetry publisher with default storage and queue managers. + /// + public TelemetryPublisher(TelemetryPublisherConfiguration config, ITelemetryCheckpointsRecoveryClient checkPointsRecoveryClient) { + _pastResults = new List(); + Config = config ?? new TelemetryPublisherConfiguration(); _pendingStorageSource = new TelemetryPendingStorageSource(); @@ -68,11 +132,16 @@ namespace Tango.Telemetry _publishThread = new Thread(PublishThreadMethod); _publishThread.IsBackground = true; + CheckpointsRecoveryClient = checkPointsRecoveryClient; + StorageManager = new TelemetryLiteDBStorageManager(); QueueManager = new TelemetryInMemoryQueueManager(); } - public TelemetryPublisher(ITelemetryStorageManager storageManager, ITelemetryQueueManager queueManager, TelemetryPublisherConfiguration config) : this(config) + /// + /// Initializes the telemetry publisher with custom storage and queue managers. + /// + public TelemetryPublisher(ITelemetryStorageManager storageManager, ITelemetryCheckpointsRecoveryClient checkPointsRecoveryClient, ITelemetryQueueManager queueManager, TelemetryPublisherConfiguration config) : this(config, checkPointsRecoveryClient) { StorageManager = storageManager; QueueManager = queueManager; @@ -82,6 +151,9 @@ namespace Tango.Telemetry #region Sources + /// + /// Registers a telemetry source, such as a streaming or historical source. + /// public void RegisterSource(ITelemetrySource source) { if (source == null) return; @@ -114,6 +186,9 @@ namespace Tango.Telemetry LogManager.Log($"Telemetry source {source.Name} registered."); } + /// + /// Callback when a telemetry streaming source emits new telemetry. + /// private void StreamingSource_TelemetryAvailable(object sender, TelemetryAvailableEventArgs e) { var source = sender as ITelemetrySource; @@ -128,6 +203,9 @@ namespace Tango.Telemetry #region Destinations + /// + /// Registers a telemetry destination, such as a cloud service or local database. + /// public void RegisterDestination(ITelemetryDestination destination) { if (destination == null) return; @@ -147,7 +225,10 @@ namespace Tango.Telemetry #region Start / Stop - public void Start() + /// + /// Starts all timers, threads, and streaming sources for publishing telemetry. + /// + public async Task Start() { if (!IsStarted) { @@ -160,6 +241,8 @@ namespace Tango.Telemetry IsStarted = true; + await StorageManager.Init(CheckpointsRecoveryClient); + if (_pendingStorageCheckTimer == null) { _pendingStorageCheckTimer = new System.Timers.Timer(); @@ -178,6 +261,15 @@ namespace Tango.Telemetry _historicalDataTimer.Start(); + if (_publishedTelemetriesCacheCleanupTimer == null) + { + _publishedTelemetriesCacheCleanupTimer = new System.Timers.Timer(); + _publishedTelemetriesCacheCleanupTimer.Interval = Config.PublishedTelemetriesCacheCleanupInterval.TotalMilliseconds; + _publishedTelemetriesCacheCleanupTimer.Elapsed += PublishedTelemetriesCacheCleanupTimer_Elapsed; + } + + _publishedTelemetriesCacheCleanupTimer.Start(); + _publishThread.Start(); InnerSources.OfType().ToList().ForEach(x => x.Start()); @@ -187,13 +279,16 @@ namespace Tango.Telemetry catch (Exception ex) { LogManager.Log(ex, "Error starting telemetry publisher."); - Stop(); - throw ex; + await Stop(); + throw; } } } - public void Stop() + /// + /// Stops all activity and releases threads and sources gracefully. + /// + public Task Stop() { if (IsStarted) { @@ -212,14 +307,19 @@ namespace Tango.Telemetry LogManager.Log(ex, $"Error while trying to stop telemetry source {x.Name}."); } }); - _pendingStorageCheckTimer.Stop(); - _historicalDataTimer.Stop(); - QueueManager.Enqueue(null); + _pendingStorageCheckTimer?.Stop(); + _historicalDataTimer?.Stop(); + QueueManager?.Enqueue(null); LogManager.Log("Telemetry publisher stopped."); } + + return Task.FromResult(true); } + /// + /// Performs runtime validation of configuration, sources, and destinations. + /// public void Validate() { // Validate all registered sources @@ -262,81 +362,148 @@ namespace Tango.Telemetry #region Timers + /// + /// Periodically invoked to process telemetry from persistent local storage. + /// private async void PendingStorageCheckTimer_Elapsed(object sender, ElapsedEventArgs e) { - _pendingStorageCheckTimer.Stop(); + LogManager.Log("Pending storage check timer elapsed. Starting flush operation for pending telemetries.", LogCategory.Debug); - LogManager.Log($"Fetching pending telemetries from storage (MaxCount: {Config.MaxPendingStorageTelemetriesPerCycle})..."); - - var batch = StorageManager.GetPendingTelemetries(Config.MaxPendingStorageTelemetriesPerCycle); + _pendingStorageCheckTimer.Stop(); - LogManager.Log($"Pending telemetries count is {batch.Count}. Publishing..."); + try + { + var results = await FlushPendingTelemetries(Config.MaxPendingStorageTelemetriesPerCycle); + LogManager.Log($"Flush operation completed. {results.Count} telemetry package(s) processed from pending storage.", LogCategory.Debug); + } + catch (Exception ex) + { + LogManager.Log(ex, LogCategory.Error, "Exception occurred while flushing pending telemetry packages."); + } + finally + { + _pendingStorageCheckTimer.Start(); + LogManager.Log("Pending storage check timer restarted.", LogCategory.Debug); + } + } - Dictionary destinationsPasses = new Dictionary(); - List results = new List(); + /// + /// Periodically invoked to fetch and push historical data from history sources. + /// + private async void HistoricalDataTimer_Elapsed(object sender, ElapsedEventArgs e) + { + LogManager.Log("Historical data timer elapsed. Checking for available capacity...", LogCategory.Debug); + _historicalDataTimer.Stop(); - foreach (var pendingTelemetry in batch) + try { - var result = await PublishTelemetryPackage(new TelemetryPublishPackage() - { - Source = _pendingStorageSource, - PendingTelemetry = pendingTelemetry, - SourceType = TelemetrySourceTypes.PendingStorage - }); + int queueCount = QueueManager.Count; + int storageCount = StorageManager.GetPendingTelemetriesCount(); - results.Add(result); + LogManager.Log($"Current queue count: {queueCount}, pending storage count: {storageCount}", LogCategory.Debug); - foreach (var d in result.DestinationsResults) + if (queueCount < Config.MaxPendingTelemetries && storageCount < Config.MaxPendingTelemetries) { - if (d.Status == TelemetryPublishResult.DestinationStatus.Passed) + foreach (var source in InnerSources.OfType().ToList()) { - destinationsPasses[d.Destination.Name] += 1; - } - } + try + { + TelemetryHistorySourceCheckPoint checkPoint = StorageManager.GetHistorySourceCheckPoint(source); - if (!IsStarted || _isDisposed) return; - } + LogManager.Log($"Evaluating history source '{source.Name}' at checkpoint time {checkPoint?.Time:u}", LogCategory.Debug); + + if (await source.CanRequestHistory(checkPoint.Time)) + { + var historyTelemetries = (await source.RequestHistory(checkPoint.Time)).OrderBy(x => x.Time).ToList(); + + LogManager.Log($"History source '{source.Name}' returned {historyTelemetries.Count} telemetry items.", LogCategory.Debug); - LogManager.Log($"Publishing pending telemetries completed after {results.Sum(x => x.TotalElapsedTime.Seconds)} seconds. Destination OK Count: {String.Join(", ", destinationsPasses.Select(x => x.Key + " -> " + x.Value))}"); + foreach (var telemetry in historyTelemetries) + { + await PushTelemetryPackageAwait(source, telemetry, TelemetrySourceTypes.ExternalStorage); + checkPoint.Time = telemetry.Time; + checkPoint.TotalCount++; + StorageManager.SetHistorySourceCheckPoint(source, checkPoint.Time, checkPoint.TotalCount); + } - _pendingStorageCheckTimer.Start(); + LogManager.Log($"Checkpoint updated for source '{source.Name}': time = {checkPoint.Time:u}, total = {checkPoint.TotalCount}", LogCategory.Debug); + } + else + { + LogManager.Log($"History request for source '{source.Name}' was not permitted at checkpoint time {checkPoint?.Time:u}", LogCategory.Debug); + } + } + catch (Exception ex) + { + LogManager.Log(ex, LogCategory.Error, $"Exception while processing history for source '{source?.Name}'"); + } + } + } + else + { + LogManager.Log("Historical data fetch skipped due to max pending telemetry limit reached.", LogCategory.Debug); + } + } + catch (Exception ex) + { + LogManager.Log(ex, LogCategory.Critical, "Unexpected error during HistoricalDataTimer_Elapsed."); + } + finally + { + _historicalDataTimer.Start(); + LogManager.Log("Historical data timer restarted.", LogCategory.Debug); + } } - private async void HistoricalDataTimer_Elapsed(object sender, ElapsedEventArgs e) + /// + /// Handles the elapsed event of the published telemetries cache cleanup timer. + /// Determines the earliest checkpoint across all history sources and removes published telemetry entries + /// older than that point to keep the cache size manageable over time. + /// + private void PublishedTelemetriesCacheCleanupTimer_Elapsed(object sender, ElapsedEventArgs e) { - _historicalDataTimer.Stop(); + LogManager.Log("Published telemetry cache cleanup timer elapsed. Starting cleanup process...", LogCategory.Debug); - LogManager.Log(""); + _publishedTelemetriesCacheCleanupTimer.Stop(); - if (QueueManager.Count < Config.MaxPendingTelemetries && StorageManager.GetPendingTelemetriesCount() < Config.MaxPendingTelemetries) + try { - foreach (var source in InnerSources.OfType().ToList()) + var checkPoints = StorageManager.GetHistorySourcesCheckPoints(); + LogManager.Log($"Retrieved {checkPoints.Count} source checkpoints for cleanup evaluation.", LogCategory.Debug); + + if (checkPoints.Count > 0) { - TelemetryHistorySourceCheckPoint checkPoint = StorageManager.GetHistorySourceCheckPoint(source); - if (await source.CanRequestHistory(checkPoint.Time)) - { - var historyTelemetries = (await source.RequestHistory(checkPoint.Time)).OrderBy(x => x.Time).ToList(); + DateTime olderThan = checkPoints.Min(x => x.Time); + LogManager.Log($"Initiating cleanup of published telemetries older than {olderThan:u}.", LogCategory.Debug); - foreach (var telemetry in historyTelemetries) - { - PushTelemetryPackage(source, telemetry, TelemetrySourceTypes.ExternalStorage); - checkPoint.Time = telemetry.Time; - checkPoint.TotalCount++; - } + StorageManager.PerformPublishedTelemetriesCleanUp(olderThan); - StorageManager.SetHistorySourceCheckPoint(source, checkPoint.Time, checkPoint.TotalCount); - } + LogManager.Log("Published telemetry cache cleanup completed successfully.", LogCategory.Debug); + } + else + { + LogManager.Log("No checkpoints found. Cleanup skipped.", LogCategory.Debug); } } - - _historicalDataTimer.Start(); + catch (Exception ex) + { + LogManager.Log(ex, LogCategory.Error, "Exception occurred during published telemetry cache cleanup."); + } + finally + { + _publishedTelemetriesCacheCleanupTimer.Start(); + LogManager.Log("Published telemetry cache cleanup timer restarted.", LogCategory.Debug); + } } #endregion #region Push - private void PushTelemetryPackage(ITelemetrySource source, ITelemetry telemetry, TelemetrySourceTypes sourceType) + /// + /// Enqueues telemetry into the system based on a source and type. + /// + private TelemetryPublishPackage PushTelemetryPackage(ITelemetrySource source, ITelemetry telemetry, TelemetrySourceTypes sourceType) { PendingTelemetry pendingTelemetry = new PendingTelemetry(); pendingTelemetry.Created = DateTime.UtcNow; @@ -345,18 +512,45 @@ namespace Tango.Telemetry pendingTelemetry.SourceType = sourceType; pendingTelemetry.TelemetryObject = telemetry; - PushTelemetryPackage(new TelemetryPublishPackage() { Source = source, PendingTelemetry = pendingTelemetry, SourceType = sourceType }); + var package = new TelemetryPublishPackage() { Source = source, PendingTelemetry = pendingTelemetry, SourceType = sourceType }; + + PushTelemetryPackage(package); + + return package; + } + + /// + /// Enqueues telemetry and returns a task that resolves when it is published. + /// + private Task PushTelemetryPackageAwait(ITelemetrySource source, ITelemetry telemetry, TelemetrySourceTypes sourceType) + { + return PushTelemetryPackage(source, telemetry, sourceType).CompletionSource.Task; } + /// + /// Enqueues an already-wrapped package for background publishing. + /// private void PushTelemetryPackage(TelemetryPublishPackage package) { QueueManager.Enqueue(package); } + /// + /// Enqueues a wrapped package and awaits publish result asynchronously. + /// + private Task PushTelemetryPackageAwait(TelemetryPublishPackage package) + { + PushTelemetryPackage(package); + return package.CompletionSource.Task; + } + #endregion #region Publish + /// + /// Background thread method to publish telemetry from the queue. + /// private async void PublishThreadMethod() { while (IsStarted) @@ -379,118 +573,360 @@ namespace Tango.Telemetry } } + // This method is responsible for publishing a telemetry package to all configured destinations. + // It handles per-destination retry logic, exponential backoff, availability checks, and result reporting. + // The goal is to guarantee eventual delivery of telemetry with robust fault tolerance and observability. protected virtual async Task PublishTelemetryPackage(TelemetryPublishPackage package) { - Stopwatch totalWatch = Stopwatch.StartNew(); + LogManager.Log($"Starting publish process for telemetry package from source '{package.Source?.Name}' with type '{package.SourceType}'", LogCategory.Debug); - var result = new TelemetryPublishResult(); + Stopwatch totalWatch = Stopwatch.StartNew(); // Start measuring total publish duration + var result = new TelemetryPublishResult(); // Result container with per-destination feedback + result.Source = package.Source; + result.SourceType = package.SourceType; - if (!IsStarted || _isDisposed) return result; + // Abort early if the publisher is inactive + if (!IsStarted || _isDisposed) + { + LogManager.Log("Publish attempt skipped because the publisher is not started or has been disposed.", LogCategory.Warning); + package.CompletionSource.SetResult(result); + return result; + } - List> properties = new List>(); + //Marking the telemetry as published to avoid duplication from streaming and history sources that can produce the same telemetry. + if (package.Source.RequiresTelemetryDuplicationTracking) + { + if (StorageManager.IsTelemetryInPublishedCache(package.PendingTelemetry.TelemetryObject)) + { + LogManager.Log("Publish attempt skipped because the telemetry was already published.", LogCategory.Warning); + package.CompletionSource.SetResult(result); + return result; + } + else + { + StorageManager.AddToPublishedTelemetryCache(package.PendingTelemetry.TelemetryObject); + } + } - properties.Add(new KeyValuePair("MachineID", Config.MachineID)); - properties.Add(new KeyValuePair("Model", Config.MachineType.ToShortName())); + // Prepare standard metadata properties attached to all telemetry sent + var telemetryName = package.PendingTelemetry.TelemetryObject.ToTelemetryName(); + + List> properties = new List>(); + properties.Add(new KeyValuePair("SerialNumber", Config.SerialNumber)); + properties.Add(new KeyValuePair("MachineType", Config.MachineType.ToShortName())); properties.Add(new KeyValuePair("Environment", Config.Environment)); + properties.Add(new KeyValuePair("Type", telemetryName)); + + //Setting telemetry package basic properties for destination.. + package.TelemetryName = telemetryName; + package.SerialNumber = Config.SerialNumber; + package.Environment = Config.Environment; + package.MachineType = Config.MachineType.ToShortName(); + + var now = DateTime.UtcNow; // Capture timestamp once for all retry logic List pendingDestinations = package.PendingTelemetry.PendingDestinations.ToList(); - //Add all destinations if streaming or external (They will be remove later if successfull) - //If source is "PendingStorage" the "PendingDestination" would be already propagated from the pending storage db. + // If this is a fresh package, initialize pending destinations if (package.SourceType == TelemetrySourceTypes.Streaming || package.SourceType == TelemetrySourceTypes.ExternalStorage) { + LogManager.Log("Evaluating destinations for initial pending destination registration...", LogCategory.Debug); foreach (var destination in Destinations) { - if (!pendingDestinations.Exists(x => x.Name == destination.Name)) + if (destination.SupportedSourceTypes.Contains(package.SourceType)) { - pendingDestinations.Add(new TelemetryPendingDestination { Name = destination.Name }); + if (!pendingDestinations.Exists(x => x.Name == destination.Name)) + { + pendingDestinations.Add(new TelemetryPendingDestination + { + Name = destination.Name, + RetryCount = 0, + LastAttempt = DateTime.MinValue, + NextEligibleAttempt = now + }); + LogManager.Log($"Added destination '{destination.Name}' to pending destinations.", LogCategory.Debug); + } } } } - foreach (var destination in Destinations.Where(x => x.Enable && x.SupportedSourceTypes.Contains(package.SourceType))) + // Try publishing to each valid destination + foreach (var destination in Destinations.Where(x => x.SupportedSourceTypes.Contains(package.SourceType))) { - if (pendingDestinations.Exists(x => x.Name == destination.Name)) + var pendingEntry = pendingDestinations.FirstOrDefault(x => x.Name == destination.Name); + if (pendingEntry == null) continue; // Skip destinations not pending for this package + + // Prepare result tracking for this destination + var destinationResult = new TelemetryPublishResult.DestinationResult(); + destinationResult.Destination = destination; + destinationResult.RetryCount = pendingEntry.RetryCount; + destinationResult.RetryDelay = TimeSpan.FromSeconds(Math.Max(0, (pendingEntry.NextEligibleAttempt - now).TotalSeconds)); + result.DestinationsResults.Add(destinationResult); + + // If we're still in a backoff delay, skip for now + if (now < pendingEntry.NextEligibleAttempt) { - Stopwatch destinationWatch = Stopwatch.StartNew(); + destinationResult.Status = TelemetryPublishResult.DestinationStatus.Postponed; + destinationResult.ElapsedTime = TimeSpan.Zero; + LogManager.Log($"Skipping '{destination.Name}' until {pendingEntry.NextEligibleAttempt:O} (backoff in effect).", LogCategory.Debug); + continue; + } - var destinationResult = new TelemetryPublishResult.DestinationResult(); - destinationResult.Destination = destination; - result.DestinationsResults.Add(destinationResult); + Stopwatch destinationWatch = Stopwatch.StartNew(); // Measure this attempt duration - try + try + { + // Remove destination from pending list so we can re-add it if needed after this attempt + pendingDestinations.RemoveAll(x => x.Name == destination.Name); + LogManager.Log($"Attempting to publish to destination '{destination.Name}'...", LogCategory.Debug); + + // Allow event handlers to cancel or inspect the publish + if (OnPublishingPackage(package, destination)) { - pendingDestinations.RemoveAll(x => x.Name == destination.Name); - if (OnPublishingPackage(package, destination)) + // Ensure destination is ready before sending + if (await destination.IsAvailable()) { - if (await destination.IsAvailable()) - { - await destination.Publish(package, properties); - OnPackagePublished(package, destination); + await destination.Publish(package, properties); // Perform publish + OnPackagePublished(package, destination); // Notify success event + + destinationWatch.Stop(); + + destinationResult.RetryDelay = TimeSpan.Zero; + destinationResult.Status = TelemetryPublishResult.DestinationStatus.Passed; + destinationResult.ElapsedTime = destinationWatch.Elapsed; + + LogManager.Log($"Successfully published to '{destination.Name}' in {destinationResult.ElapsedTime.TotalMilliseconds} ms."); + } + else + { + // Mark as temporarily unavailable and schedule retry + destinationWatch.Stop(); + destinationResult.Status = TelemetryPublishResult.DestinationStatus.Unavailable; + destinationResult.ElapsedTime = destinationWatch.Elapsed; + + LogManager.Log($"Destination '{destination.Name}' is unavailable.", LogCategory.Warning); - destinationWatch.Stop(); - destinationResult.Status = TelemetryPublishResult.DestinationStatus.Passed; - destinationResult.ElapsedTime = destinationWatch.Elapsed; + if (destination.SupportedSourceTypes.Contains(TelemetrySourceTypes.PendingStorage)) + { + pendingEntry.RetryCount++; + pendingEntry.LastAttempt = now; + int delay = Math.Min((int)Math.Pow(2, pendingEntry.RetryCount), (int)Config.MaxExponentialBackoff.TotalSeconds); + pendingEntry.NextEligibleAttempt = now.AddSeconds(delay); + LogManager.Log($"Scheduled retry to '{destination.Name}' in {delay} seconds.", LogCategory.Debug); + pendingDestinations.Add(pendingEntry); } else { - destinationWatch.Stop(); - destinationResult.Status = TelemetryPublishResult.DestinationStatus.Unavailable; - destinationResult.ElapsedTime = destinationWatch.Elapsed; - - if (destination.SupportedSourceTypes.Contains(TelemetrySourceTypes.PendingStorage)) - { - if (!pendingDestinations.Exists(x => x.Name == destination.Name)) - { - pendingDestinations.Add(new TelemetryPendingDestination() { Name = destination.Name }); - } - } + LogManager.Log($"'{destination.Name}' is not retryable. Removed from pending.", LogCategory.Debug); } } } - catch (Exception ex) - { - destinationWatch.Stop(); - destinationResult.Status = TelemetryPublishResult.DestinationStatus.Failed; - destinationResult.Error = ex; - destinationResult.ElapsedTime = destinationWatch.Elapsed; - - LogManager.Log(ex, $"Error publishing telemetry package to destination {destination.Name}."); + } + catch (Exception ex) + { + // Log unexpected failure and retry if supported + destinationWatch.Stop(); + destinationResult.Status = TelemetryPublishResult.DestinationStatus.Failed; + destinationResult.Error = ex; + destinationResult.ElapsedTime = destinationWatch.Elapsed; - OnPackagePublishFailed(package, destination, ex); + LogManager.Log(ex, $"Error publishing telemetry to '{destination.Name}'."); + OnPackagePublishFailed(package, destination, ex); - if (destination.SupportedSourceTypes.Contains(TelemetrySourceTypes.PendingStorage)) - { - if (!pendingDestinations.Exists(x => x.Name == destination.Name)) - { - pendingDestinations.Add(new TelemetryPendingDestination() { Name = destination.Name }); - } - } + if (destination.SupportedSourceTypes.Contains(TelemetrySourceTypes.PendingStorage)) + { + pendingEntry.RetryCount++; + pendingEntry.LastAttempt = now; + int delay = Math.Min((int)Math.Pow(2, pendingEntry.RetryCount), (int)Config.MaxExponentialBackoff.TotalSeconds); + pendingEntry.NextEligibleAttempt = now.AddSeconds(delay); + LogManager.Log($"Scheduled retry to '{destination.Name}' in {delay} seconds due to failure.", LogCategory.Debug); + pendingDestinations.Add(pendingEntry); + } + else + { + LogManager.Log($"'{destination.Name}' is not retryable. Removed from pending after failure.", LogCategory.Debug); } } } + // Save retry state back into the package package.PendingTelemetry.PendingDestinations = pendingDestinations; - if (package.SourceType == TelemetrySourceTypes.PendingStorage && package.PendingTelemetry.PendingDestinations.Count == 0) + // Remove from storage if all destinations succeeded; otherwise persist state + if (package.PendingTelemetry.PendingDestinations.Count == 0) { + LogManager.Log("Deleting successfully published telemetry from storage.", LogCategory.Debug); StorageManager.DeletePendingTelemetry(package.PendingTelemetry); } else { + LogManager.Log("Saving telemetry package for future retry or tracking.", LogCategory.Debug); StorageManager.UpsertPendingTelemetry(package.PendingTelemetry); } + // Finalize result and notify completion totalWatch.Stop(); result.TotalElapsedTime = totalWatch.Elapsed; + LogManager.Log($"Completed publish process for telemetry from source '{package.Source?.Name}' in {result.TotalElapsedTime.TotalMilliseconds} ms.", LogCategory.Debug); + + //Add results for reporting + _pastResults.Add(result); + + //Set task result for once that are awaiting from outside this method. + package.CompletionSource.SetResult(result); + + //Raising final event + OnPublishResultAvailable(package, result); return result; } + #endregion + + #region Flush + + /// + /// Flushes up to the specified number of pending telemetries from local storage, + /// attempting to publish them immediately. This can be used to force a retry of previously failed or postponed telemetry packages. + /// + /// The maximum number of pending telemetry packages to flush. + /// + /// A task that represents the asynchronous flush operation, returning a list of publish results for the flushed packages. + /// + public async Task> FlushPendingTelemetries(int maxCount) + { + if (!IsStarted || _isDisposed) + { + LogManager.Log("FlushPendingTelemetries called while publisher is not started or already disposed. Operation aborted.", LogCategory.Warning); + return new List(); + } + + var batch = StorageManager.GetPendingTelemetries(maxCount); + LogManager.Log($"Flushing {batch.Count} pending telemetry package(s).", LogCategory.Info); + + List results = new List(); + + foreach (var pendingTelemetry in batch) + { + try + { + var package = new TelemetryPublishPackage() + { + Source = _pendingStorageSource, + PendingTelemetry = pendingTelemetry, + SourceType = TelemetrySourceTypes.PendingStorage + }; + + var result = await PushTelemetryPackageAwait(package); + results.Add(result); + + LogManager.Log( + $"Flushed telemetry to destinations: {string.Join(", ", result.DestinationsResults.Select(r => $"{r.Destination.Name}={r.Status}"))}", + LogCategory.Debug); + } + catch (Exception ex) + { + LogManager.Log(ex, LogCategory.Error, "Exception occurred while flushing a pending telemetry package."); + } + + if (!IsStarted || _isDisposed) + { + LogManager.Log("Flush operation interrupted: publisher is no longer active.", LogCategory.Warning); + return results; + } + } + + LogManager.Log("FlushPendingTelemetries completed successfully.", LogCategory.Info); + return results; + } + + #endregion + + #region Reporting + + /// + /// Generates a detailed telemetry report summarizing the current state of the telemetry system. + /// The report includes statistics on published and pending telemetry, as well as per-source and per-destination results. + /// + public Task GetTelemetryReport() + { + return Task.Factory.StartNew(() => + { + TelemetryReport report = new TelemetryReport + { + GeneratedAt = DateTime.UtcNow, + TotalPending = StorageManager.GetPendingTelemetriesCount() + }; + + var results = _pastResults.ToList(); + report.TotalPublished = results.Count; + + foreach (var result in results) + { + var sourceType = result.SourceType; + var sourceName = result.Source?.Name ?? "UnknownSource"; + + if (!report.SourceTypes.TryGetValue(sourceType, out var sourceTypeSummary)) + { + sourceTypeSummary = new SourceTypeSummary + { + SourceType = sourceType + }; + report.SourceTypes[sourceType] = sourceTypeSummary; + } + + if (!sourceTypeSummary.Sources.TryGetValue(sourceName, out var sourceSummary)) + { + sourceSummary = new SourceSummary + { + SourceName = sourceName + }; + sourceTypeSummary.Sources[sourceName] = sourceSummary; + } + + foreach (var destResult in result.DestinationsResults) + { + var destName = destResult.Destination.Name; + + if (!sourceSummary.Destinations.TryGetValue(destName, out var destSummary)) + { + destSummary = new DestinationStatusSummary + { + DestinationName = destName + }; + sourceSummary.Destinations[destName] = destSummary; + } + + switch (destResult.Status) + { + case TelemetryPublishResult.DestinationStatus.Passed: + destSummary.Passed++; + break; + case TelemetryPublishResult.DestinationStatus.Failed: + destSummary.Failed++; + break; + case TelemetryPublishResult.DestinationStatus.Postponed: + destSummary.Postponed++; + break; + case TelemetryPublishResult.DestinationStatus.Unavailable: + destSummary.Unavailable++; + break; + } + } + } + + return report; + }); + } + + #endregion #region Virtual Methods + /// + /// Called before a package is published to allow for canceling or preprocessing. + /// protected virtual bool OnPublishingPackage(TelemetryPublishPackage package, ITelemetryDestination destination) { try @@ -505,6 +941,9 @@ namespace Tango.Telemetry } } + /// + /// Called after a package has been successfully delivered to a destination. + /// protected virtual void OnPackagePublished(TelemetryPublishPackage package, ITelemetryDestination destination) { try @@ -514,6 +953,9 @@ namespace Tango.Telemetry catch { } } + /// + /// Called after a failed attempt to publish a telemetry package. + /// protected virtual void OnPackagePublishFailed(TelemetryPublishPackage package, ITelemetryDestination destination, Exception exception) { try @@ -523,10 +965,27 @@ namespace Tango.Telemetry catch { } } + /// + /// Called when a publish result is available after a complete publish pass. + /// + /// The package. + /// The result. + protected virtual void OnPublishResultAvailable(TelemetryPublishPackage package, TelemetryPublishResult result) + { + try + { + PublishResultAvailable?.Invoke(this, new TelemetryPublishResultAvailableEventArgs() { Package = package, PublishResult = result }); + } + catch { } + } + #endregion #region Dispose + /// + /// Disposes all sources, destinations, timers, and gracefully shuts down. + /// public void Dispose() { if (!_isDisposed) diff --git a/Software/Visual_Studio/Tango.Telemetry/TelemetryPublisherAdvanced.cs b/Software/Visual_Studio/Tango.Telemetry/TelemetryPublisherAdvanced.cs index c4e7d3d3e..971afa864 100644 --- a/Software/Visual_Studio/Tango.Telemetry/TelemetryPublisherAdvanced.cs +++ b/Software/Visual_Studio/Tango.Telemetry/TelemetryPublisherAdvanced.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Tango.Integration.Operation; +using Tango.Logging; namespace Tango.Telemetry { @@ -21,15 +22,26 @@ namespace Tango.Telemetry } + // This method handles the complete lifecycle of publishing a telemetry package to all supported destinations. + // It supports exponential backoff retry logic, destination filtering, and persistence of failed deliveries + // to ensure fault tolerance and guaranteed eventual delivery across multiple transports. protected override async Task PublishTelemetryPackage(TelemetryPublishPackage package) { - Stopwatch totalWatch = Stopwatch.StartNew(); + LogManager.Log($"Starting publish for package from source '{package.Source?.Name}' of type '{package.SourceType}'", LogCategory.Debug); + Stopwatch totalWatch = Stopwatch.StartNew(); var result = new TelemetryPublishResult(); - if (!IsStarted || _isDisposed) return result; + // Exit early if the publisher has been stopped or disposed to avoid invalid operations. + if (!IsStarted || _isDisposed) + { + LogManager.Log("Publisher not active. Skipping.", LogCategory.Warning); + package.CompletionSource.SetResult(result); + return result; + } - List> properties = new List> + // Prepare metadata headers to be attached to the telemetry, which are common to all destinations. + var properties = new List> { new KeyValuePair("MachineID", Config.MachineID), new KeyValuePair("Model", Config.MachineType.ToShortName()), @@ -39,13 +51,15 @@ namespace Tango.Telemetry var now = DateTime.UtcNow; var pendingDestinations = package.PendingTelemetry.PendingDestinations.ToList(); - // For Streaming/External: initialize pending destinations list (used if publishing fails) + // If this is a newly generated package, not a retry, initialize its destination list. + // This ensures we only attempt to publish to destinations that support this telemetry type. if (package.SourceType == TelemetrySourceTypes.Streaming || package.SourceType == TelemetrySourceTypes.ExternalStorage) { foreach (var dest in Destinations) { - if (!pendingDestinations.Any(x => x.Name == dest.Name)) + if (dest.SupportedSourceTypes.Contains(package.SourceType) && !pendingDestinations.Any(x => x.Name == dest.Name)) { + // Initialize with default retry tracking values. pendingDestinations.Add(new TelemetryPendingDestination { Name = dest.Name, @@ -53,38 +67,40 @@ namespace Tango.Telemetry LastAttempt = DateTime.MinValue, NextEligibleAttempt = now }); + LogManager.Log($"Registered destination '{dest.Name}' for initial delivery.", LogCategory.Debug); } } } + // Iterate over each valid destination, applying retry backoff and delivery logic. foreach (var destination in Destinations.Where(x => x.SupportedSourceTypes.Contains(package.SourceType))) { var pendingEntry = pendingDestinations.FirstOrDefault(x => x.Name == destination.Name); - - if (pendingEntry == null) - continue; + if (pendingEntry == null) continue; Stopwatch destinationWatch = Stopwatch.StartNew(); - var destinationResult = new TelemetryPublishResult.DestinationResult(); destinationResult.Destination = destination; result.DestinationsResults.Add(destinationResult); - // Respect backoff timing + // If the destination is in a cool-down period due to previous failures, skip for now. if (now < pendingEntry.NextEligibleAttempt) { destinationWatch.Stop(); destinationResult.Status = TelemetryPublishResult.DestinationStatus.Postponed; destinationResult.ElapsedTime = destinationWatch.Elapsed; + LogManager.Log($"Skipping '{destination.Name}' until {pendingEntry.NextEligibleAttempt:O} (backoff in effect).", LogCategory.Debug); continue; } try { + LogManager.Log($"Attempting publish to '{destination.Name}'...", LogCategory.Debug); if (OnPublishingPackage(package, destination)) { if (await destination.IsAvailable()) { + // Send the telemetry payload await destination.Publish(package, properties); OnPackagePublished(package, destination); @@ -92,87 +108,90 @@ namespace Tango.Telemetry destinationResult.Status = TelemetryPublishResult.DestinationStatus.Passed; destinationResult.ElapsedTime = destinationWatch.Elapsed; - // On success: remove entry from pending list + LogManager.Log($"Successfully published to '{destination.Name}' in {destinationResult.ElapsedTime.TotalMilliseconds} ms."); + + // Remove from retry queue since delivery succeeded. pendingDestinations.RemoveAll(x => x.Name == destination.Name); } else { + // Destination is temporarily unreachable; apply retry logic if supported. destinationWatch.Stop(); destinationResult.Status = TelemetryPublishResult.DestinationStatus.Unavailable; destinationResult.ElapsedTime = destinationWatch.Elapsed; - // Only track retry state if retry is supported + LogManager.Log($"'{destination.Name}' unavailable.", LogCategory.Warning); if (destination.SupportedSourceTypes.Contains(TelemetrySourceTypes.PendingStorage)) { - if (pendingEntry == null) - { - pendingEntry = new TelemetryPendingDestination { Name = destination.Name }; - pendingDestinations.Add(pendingEntry); - } - pendingEntry.RetryCount++; pendingEntry.LastAttempt = now; - - // Apply exponential backoff - int delaySeconds = Math.Min((int)Math.Pow(2, pendingEntry.RetryCount), (int)MaxExponentialBackoff.TotalSeconds); - pendingEntry.NextEligibleAttempt = now.AddSeconds(delaySeconds); + int delay = Math.Min((int)Math.Pow(2, pendingEntry.RetryCount), (int)MaxExponentialBackoff.TotalSeconds); + pendingEntry.NextEligibleAttempt = now.AddSeconds(delay); + LogManager.Log($"Scheduled retry to '{destination.Name}' in {delay} seconds.", LogCategory.Debug); } else { - // Remove if not retryable + // Remove from retry list if the destination is not retryable. pendingDestinations.RemoveAll(x => x.Name == destination.Name); + LogManager.Log($"'{destination.Name}' is not retryable. Removed from pending.", LogCategory.Debug); } } } } catch (Exception ex) { + // Network, serialization, or other critical failures are handled here. destinationWatch.Stop(); destinationResult.Status = TelemetryPublishResult.DestinationStatus.Failed; destinationResult.Error = ex; destinationResult.ElapsedTime = destinationWatch.Elapsed; - LogManager.Log(ex, $"Error publishing telemetry package to destination {destination.Name}."); + LogManager.Log(ex, $"Error publishing to '{destination.Name}'."); OnPackagePublishFailed(package, destination, ex); - // Only track retry state if retry is supported if (destination.SupportedSourceTypes.Contains(TelemetrySourceTypes.PendingStorage)) { - if (pendingEntry == null) - { - pendingEntry = new TelemetryPendingDestination { Name = destination.Name }; - pendingDestinations.Add(pendingEntry); - } - + // Retry and Backoff Logic: + // -------------------------- + // Each destination maintains its own retry metadata: RetryCount, LastAttempt, and NextEligibleAttempt. + // On each failed delivery attempt, RetryCount is incremented and a delay is calculated using exponential backoff (2^RetryCount seconds). + // This delay is capped using MaxExponentialBackoff to prevent runaway delays. + // The system checks NextEligibleAttempt before retrying, and skips publishing if the backoff period has not yet elapsed. + // This ensures robust delivery across unstable networks and prevents retry storms. pendingEntry.RetryCount++; pendingEntry.LastAttempt = now; - - // Apply exponential backoff - int delaySeconds = Math.Min((int)Math.Pow(2, pendingEntry.RetryCount), (int)MaxExponentialBackoff.TotalSeconds); - pendingEntry.NextEligibleAttempt = now.AddSeconds(delaySeconds); + int delay = Math.Min((int)Math.Pow(2, pendingEntry.RetryCount), (int)MaxExponentialBackoff.TotalSeconds); + pendingEntry.NextEligibleAttempt = now.AddSeconds(delay); + LogManager.Log($"Scheduled retry to '{destination.Name}' in {delay} seconds due to failure.", LogCategory.Debug); } else { - // Remove if not retryable pendingDestinations.RemoveAll(x => x.Name == destination.Name); + LogManager.Log($"'{destination.Name}' is not retryable. Removed from pending after failure.", LogCategory.Debug); } } } + // Save updated retry state for this telemetry package package.PendingTelemetry.PendingDestinations = new List(pendingDestinations); + // Determine whether to remove the telemetry or persist it based on remaining destinations if (package.SourceType == TelemetrySourceTypes.PendingStorage && !pendingDestinations.Any()) { + LogManager.Log("Telemetry successfully delivered to all destinations. Deleting.", LogCategory.Debug); StorageManager.DeletePendingTelemetry(package.PendingTelemetry); } else { - StorageManager.DeletePendingTelemetry(package.PendingTelemetry); + LogManager.Log("Updating pending telemetry record.", LogCategory.Debug); + StorageManager.UpsertPendingTelemetry(package.PendingTelemetry); } totalWatch.Stop(); result.TotalElapsedTime = totalWatch.Elapsed; + LogManager.Log($"Completed publish for source '{package.Source?.Name}' in {result.TotalElapsedTime.TotalMilliseconds} ms.", LogCategory.Debug); + package.CompletionSource.SetResult(result); return result; } } diff --git a/Software/Visual_Studio/Tango.Telemetry/TelemetryPublisherConfiguration.cs b/Software/Visual_Studio/Tango.Telemetry/TelemetryPublisherConfiguration.cs index 4a3776b87..a9f4954dd 100644 --- a/Software/Visual_Studio/Tango.Telemetry/TelemetryPublisherConfiguration.cs +++ b/Software/Visual_Studio/Tango.Telemetry/TelemetryPublisherConfiguration.cs @@ -1,22 +1,62 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Tango.BL.Enumerations; namespace Tango.Telemetry { public class TelemetryPublisherConfiguration { - public String MachineID { get; set; } + /// + /// Unique identifier of the machine sending telemetry. Used for tagging and routing. + /// + public String SerialNumber { get; set; } + + /// + /// Enum representing the type of machine (e.g., X1, X4). Helps differentiate models in telemetry. + /// public MachineTypes MachineType { get; set; } + + /// + /// Environment in which the telemetry is being published (e.g., Production, QA, Dev). + /// public String Environment { get; set; } + + /// + /// Interval for checking and reprocessing failed/pending telemetry from local storage. + /// public TimeSpan PendingStorageCheckInterval { get; set; } + + /// + /// Maximum number of pending telemetry records to process in a single retry cycle. + /// public int MaxPendingStorageTelemetriesPerCycle { get; set; } + + /// + /// Frequency at which historical sources are polled to request backlogged or missed telemetry. + /// public TimeSpan HistorySourcesRequestInterval { get; set; } + + /// + /// Maximum number of telemetry packages allowed in memory queues before rejecting new packages. + /// public int MaxPendingTelemetries { get; set; } + /// + /// Whether exponential backoff should be applied to retry logic per destination. + /// + public bool EnableBackoff { get; set; } + + /// + /// The maximum amount of time to delay retries during exponential backoff. + /// + public TimeSpan MaxExponentialBackoff { get; set; } = TimeSpan.FromHours(1); + + /// + /// Gets or sets the interval at which the published telemetry cache cleanup process should occur. + /// This defines how frequently old published telemetry entries are eligible for pruning, + /// based on their publication timestamp. + /// + public TimeSpan PublishedTelemetriesCacheCleanupInterval { get; set; } = TimeSpan.FromHours(1); + public TelemetryPublisherConfiguration() { PendingStorageCheckInterval = TimeSpan.FromMinutes(1); @@ -27,8 +67,8 @@ namespace Tango.Telemetry public void Validate() { - if (!MachineID.IsNotNullOrEmpty()) - throw new ArgumentNullException(nameof(MachineID), "MachineID is not set or empty."); + if (!SerialNumber.IsNotNullOrEmpty()) + throw new ArgumentNullException(nameof(SerialNumber), "SerialNumber is not set or empty."); if (!Environment.IsNotNullOrEmpty()) throw new ArgumentNullException(nameof(Environment), "Environment is not set or empty."); diff --git a/Software/Visual_Studio/Tango.Telemetry/TelemetrySourceTypes.cs b/Software/Visual_Studio/Tango.Telemetry/TelemetrySourceTypes.cs index 1ed80d5c9..3eb6ad6e0 100644 --- a/Software/Visual_Studio/Tango.Telemetry/TelemetrySourceTypes.cs +++ b/Software/Visual_Studio/Tango.Telemetry/TelemetrySourceTypes.cs @@ -6,10 +6,25 @@ using System.Threading.Tasks; namespace Tango.Telemetry { + /// + /// Specifies the origin type of a telemetry data package. + /// Used to control routing and behavior during telemetry publishing. + /// public enum TelemetrySourceTypes { + /// + /// Telemetry generated in real-time by a live telemetry streaming source. + /// Streaming, + + /// + /// Telemetry loaded on demand from historical sources such as logs or external databases. + /// ExternalStorage, - PendingStorage, + + /// + /// Telemetry restored from a local storage buffer due to prior publish failure. + /// + PendingStorage } } diff --git a/Software/Visual_Studio/Tango.Telemetry/app.config b/Software/Visual_Studio/Tango.Telemetry/app.config new file mode 100644 index 000000000..601f6dd75 --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/app.config @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Software/Visual_Studio/Tango.Telemetry/packages.config b/Software/Visual_Studio/Tango.Telemetry/packages.config index 1a4b2c3c8..027500d67 100644 --- a/Software/Visual_Studio/Tango.Telemetry/packages.config +++ b/Software/Visual_Studio/Tango.Telemetry/packages.config @@ -6,14 +6,18 @@ + - - + + + + + @@ -22,18 +26,25 @@ - + + + + + + + + @@ -43,8 +54,10 @@ + + @@ -62,6 +75,7 @@ + @@ -71,5 +85,6 @@ - + + \ No newline at end of file diff --git a/Software/Visual_Studio/Tango.sln b/Software/Visual_Studio/Tango.sln index 4754e8e23..6133a98d5 100644 --- a/Software/Visual_Studio/Tango.sln +++ b/Software/Visual_Studio/Tango.sln @@ -485,6 +485,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tango.ColorMeasurementsGene EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tango.Portal", "Web\Tango.Portal\Tango.Portal.csproj", "{F63B7F20-FAA3-40A6-8E34-F02369189DF8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tango.Telemetry", "Tango.Telemetry\Tango.Telemetry.csproj", "{AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tango.TelemetryTester.CLI", "Utilities\Tango.TelemetryTester.CLI\Tango.TelemetryTester.CLI.csproj", "{DB3A1470-994B-46DF-9BE8-F905CDB97A3A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tango.Telemetry.Tester.IOT.CLI", "Utilities\Tango.Telemetry.Tester.IOT.CLI\Tango.Telemetry.Tester.IOT.CLI.csproj", "{C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution AppVeyor|Any CPU = AppVeyor|Any CPU @@ -25619,6 +25625,366 @@ Global {F63B7F20-FAA3-40A6-8E34-F02369189DF8}.X1|x64.Build.0 = Release|Any CPU {F63B7F20-FAA3-40A6-8E34-F02369189DF8}.X1|x86.ActiveCfg = Release|Any CPU {F63B7F20-FAA3-40A6-8E34-F02369189DF8}.X1|x86.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.AppVeyor|Any CPU.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.AppVeyor|Any CPU.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.AppVeyor|ARM.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.AppVeyor|ARM.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.AppVeyor|ARM64.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.AppVeyor|ARM64.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.AppVeyor|x64.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.AppVeyor|x64.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.AppVeyor|x86.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.AppVeyor|x86.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Debug|ARM.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Debug|ARM.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Debug|ARM64.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Debug|x64.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Debug|x64.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Debug|x86.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Debug|x86.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka_Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka_Debug|Any CPU.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka_Debug|ARM.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka_Debug|ARM.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka_Debug|ARM64.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka_Debug|ARM64.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka_Debug|x64.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka_Debug|x64.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka_Debug|x86.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka_Debug|x86.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka|Any CPU.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka|Any CPU.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka|ARM.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka|ARM.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka|ARM64.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka|ARM64.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka|x64.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka|x64.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka|x86.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Eureka|x86.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.0|Any CPU.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.0|Any CPU.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.0|ARM.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.0|ARM.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.0|ARM64.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.0|ARM64.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.0|x64.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.0|x64.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.0|x86.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.0|x86.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.5|Any CPU.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.5|Any CPU.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.5|ARM.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.5|ARM.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.5|ARM64.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.5|ARM64.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.5|x64.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.5|x64.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.5|x86.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.5|x86.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.6.1|Any CPU.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.6.1|Any CPU.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.6.1|ARM.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.6.1|ARM.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.6.1|ARM64.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.6.1|ARM64.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.6.1|x64.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.6.1|x64.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.6.1|x86.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release 4.6.1|x86.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release|Any CPU.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release|ARM.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release|ARM.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release|ARM64.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release|ARM64.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release|x64.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release|x64.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release|x86.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.Release|x86.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS_Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS_Debug|Any CPU.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS_Debug|ARM.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS_Debug|ARM.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS_Debug|ARM64.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS_Debug|ARM64.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS_Debug|x64.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS_Debug|x64.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS_Debug|x86.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS_Debug|x86.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS|Any CPU.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS|Any CPU.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS|ARM.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS|ARM.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS|ARM64.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS|ARM64.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS|x64.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS|x64.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS|x86.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.TS|x86.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1_Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1_Debug|Any CPU.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1_Debug|ARM.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1_Debug|ARM.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1_Debug|ARM64.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1_Debug|ARM64.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1_Debug|x64.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1_Debug|x64.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1_Debug|x86.ActiveCfg = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1_Debug|x86.Build.0 = Debug|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1|Any CPU.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1|Any CPU.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1|ARM.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1|ARM.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1|ARM64.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1|ARM64.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1|x64.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1|x64.Build.0 = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1|x86.ActiveCfg = Release|Any CPU + {AF593663-D4E9-4A14-A3F2-FEA57F30E9E6}.X1|x86.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.AppVeyor|Any CPU.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.AppVeyor|Any CPU.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.AppVeyor|ARM.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.AppVeyor|ARM.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.AppVeyor|ARM64.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.AppVeyor|ARM64.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.AppVeyor|x64.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.AppVeyor|x64.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.AppVeyor|x86.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.AppVeyor|x86.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Debug|ARM.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Debug|ARM.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Debug|ARM64.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Debug|x64.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Debug|x64.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Debug|x86.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Debug|x86.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka_Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka_Debug|Any CPU.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka_Debug|ARM.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka_Debug|ARM.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka_Debug|ARM64.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka_Debug|ARM64.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka_Debug|x64.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka_Debug|x64.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka_Debug|x86.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka_Debug|x86.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka|Any CPU.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka|Any CPU.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka|ARM.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka|ARM.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka|ARM64.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka|ARM64.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka|x64.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka|x64.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka|x86.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Eureka|x86.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.0|Any CPU.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.0|Any CPU.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.0|ARM.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.0|ARM.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.0|ARM64.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.0|ARM64.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.0|x64.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.0|x64.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.0|x86.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.0|x86.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.5|Any CPU.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.5|Any CPU.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.5|ARM.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.5|ARM.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.5|ARM64.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.5|ARM64.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.5|x64.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.5|x64.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.5|x86.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.5|x86.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.6.1|Any CPU.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.6.1|Any CPU.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.6.1|ARM.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.6.1|ARM.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.6.1|ARM64.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.6.1|ARM64.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.6.1|x64.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.6.1|x64.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.6.1|x86.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release 4.6.1|x86.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release|Any CPU.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release|ARM.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release|ARM.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release|ARM64.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release|ARM64.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release|x64.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release|x64.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release|x86.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.Release|x86.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS_Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS_Debug|Any CPU.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS_Debug|ARM.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS_Debug|ARM.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS_Debug|ARM64.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS_Debug|ARM64.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS_Debug|x64.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS_Debug|x64.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS_Debug|x86.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS_Debug|x86.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS|Any CPU.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS|Any CPU.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS|ARM.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS|ARM.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS|ARM64.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS|ARM64.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS|x64.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS|x64.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS|x86.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.TS|x86.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1_Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1_Debug|Any CPU.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1_Debug|ARM.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1_Debug|ARM.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1_Debug|ARM64.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1_Debug|ARM64.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1_Debug|x64.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1_Debug|x64.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1_Debug|x86.ActiveCfg = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1_Debug|x86.Build.0 = Debug|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1|Any CPU.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1|Any CPU.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1|ARM.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1|ARM.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1|ARM64.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1|ARM64.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1|x64.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1|x64.Build.0 = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1|x86.ActiveCfg = Release|Any CPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A}.X1|x86.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.AppVeyor|Any CPU.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.AppVeyor|Any CPU.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.AppVeyor|ARM.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.AppVeyor|ARM.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.AppVeyor|ARM64.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.AppVeyor|ARM64.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.AppVeyor|x64.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.AppVeyor|x64.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.AppVeyor|x86.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.AppVeyor|x86.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Debug|ARM.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Debug|ARM.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Debug|ARM64.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Debug|x64.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Debug|x64.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Debug|x86.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Debug|x86.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka_Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka_Debug|Any CPU.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka_Debug|ARM.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka_Debug|ARM.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka_Debug|ARM64.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka_Debug|ARM64.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka_Debug|x64.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka_Debug|x64.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka_Debug|x86.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka_Debug|x86.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka|Any CPU.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka|Any CPU.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka|ARM.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka|ARM.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka|ARM64.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka|ARM64.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka|x64.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka|x64.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka|x86.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Eureka|x86.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.0|Any CPU.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.0|Any CPU.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.0|ARM.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.0|ARM.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.0|ARM64.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.0|ARM64.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.0|x64.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.0|x64.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.0|x86.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.0|x86.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.5|Any CPU.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.5|Any CPU.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.5|ARM.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.5|ARM.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.5|ARM64.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.5|ARM64.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.5|x64.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.5|x64.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.5|x86.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.5|x86.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.6.1|Any CPU.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.6.1|Any CPU.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.6.1|ARM.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.6.1|ARM.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.6.1|ARM64.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.6.1|ARM64.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.6.1|x64.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.6.1|x64.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.6.1|x86.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release 4.6.1|x86.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release|Any CPU.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release|ARM.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release|ARM.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release|ARM64.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release|ARM64.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release|x64.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release|x64.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release|x86.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.Release|x86.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS_Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS_Debug|Any CPU.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS_Debug|ARM.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS_Debug|ARM.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS_Debug|ARM64.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS_Debug|ARM64.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS_Debug|x64.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS_Debug|x64.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS_Debug|x86.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS_Debug|x86.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS|Any CPU.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS|Any CPU.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS|ARM.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS|ARM.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS|ARM64.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS|ARM64.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS|x64.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS|x64.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS|x86.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.TS|x86.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1_Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1_Debug|Any CPU.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1_Debug|ARM.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1_Debug|ARM.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1_Debug|ARM64.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1_Debug|ARM64.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1_Debug|x64.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1_Debug|x64.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1_Debug|x86.ActiveCfg = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1_Debug|x86.Build.0 = Debug|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1|Any CPU.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1|Any CPU.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1|ARM.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1|ARM.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1|ARM64.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1|ARM64.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1|x64.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1|x64.Build.0 = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1|x86.ActiveCfg = Release|Any CPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A}.X1|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -25794,8 +26160,11 @@ Global {7A30B35F-94DC-4A9C-B9D2-CB5CAA735788} = {4EE6DBA1-71BC-49E2-8DC7-266487E61050} {04FCA0BA-A64B-4C6A-A011-A94CE335D616} = {5F6BBAA8-EAD0-4B18-97E5-55B4F56DD760} {F63B7F20-FAA3-40A6-8E34-F02369189DF8} = {59B2E8DA-2D5D-48FA-9A96-F53BDB7EF7A9} + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A} = {5F6BBAA8-EAD0-4B18-97E5-55B4F56DD760} + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A} = {5F6BBAA8-EAD0-4B18-97E5-55B4F56DD760} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution + EnterpriseLibraryConfigurationToolBinariesPathV6 = packages\EnterpriseLibrary.TransientFaultHandling.6.0.1304.0\lib\portable-net45+win+wp8 SolutionGuid = {7986F7F4-A86A-4994-B1B6-0988D7F057B6} BuildVersion_BuildVersioningStyle = None.None.Increment.DeltaBaseYearDayOfYear BuildVersion_UpdateAssemblyVersion = True diff --git a/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/App.config b/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/App.config new file mode 100644 index 000000000..959eb4686 --- /dev/null +++ b/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/App.config @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Program.cs b/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Program.cs new file mode 100644 index 000000000..cb8a79288 --- /dev/null +++ b/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Program.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Tango.BL.Enumerations; +using Tango.Telemetry.Destinations; +using Tango.Telemetry.Sources; + +namespace Tango.Telemetry.Tester.IOT.CLI +{ + class Program + { + static void Main(string[] args) + { + TelemetryPublisher publisher = new TelemetryPublisher(new TelemetryPublisherConfiguration() + { + Environment = "DEV", + SerialNumber = "dev-machine", + MachineType = MachineTypes.TS1800, + HistorySourcesRequestInterval = TimeSpan.FromSeconds(1), + EnableBackoff = false, + + }, null); + + (publisher.StorageManager as TelemetryLiteDBStorageManager).EnableCheckPointsRecovery = false; + + publisher.RegisterSource(new JobRunsTestSource()); + publisher.RegisterDestination(new TelemetryAzureHubDestination("HostName=iot-twine-dev-weu.azure-devices.net;DeviceId=telemetry-dev-01;SharedAccessKey=cZhCMhiVL+TF7p13fpX+lFmyxoy8ZqCkbxUwumWw18Q=")); + + publisher.PublishResultAvailable += Publisher_PublishResultAvailable; + + publisher.Start().GetAwaiter().GetResult(); + + Console.Clear(); + Console.WriteLine("=== Telemetry IoT Hub Test Utility ==="); + Console.WriteLine($"Publisher started. Streaming every {publisher.Config.HistorySourcesRequestInterval.TotalSeconds} seconds."); + Console.WriteLine("Press any key to stop streaming data..."); + + Console.ReadKey(); + + Console.WriteLine("Disposing publisher..."); + publisher.Dispose(); + } + + private static void Publisher_PublishResultAvailable(object sender, TelemetryPublishResultAvailableEventArgs e) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine($"Package publish result available:\n{e.PublishResult.ToString()}"); + + if (e.PublishResult.DestinationsResults.Any(d => d.Status == TelemetryPublishResult.DestinationStatus.Failed)) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("One or more destinations failed to receive the package."); + } + else + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("Package successfully published."); + } + Console.ResetColor(); + + Console.WriteLine("Press any key to stop streaming data..."); + } + } + + [TelemetryName("JobRun")] + public class JobRunTestTelemetry : TelemetryBase + { + public String JobName { get; set; } + public String Thread { get; set; } + public double Length { get; set; } + } + + public class JobRunsTestSource : ITelemetryHistorySource + { + private int counter = 1; + + public string Name { get; } = "Persons Source"; + public bool RequiresTelemetryDuplicationTracking { get; } = false; + + public Task CanRequestHistory(DateTime from) + { + return Task.FromResult(true); + } + + public void Dispose() + { + + } + + public Task> RequestHistory(DateTime from) + { + return Task. + FromResult>(new List() + { + new JobRunTestTelemetry() + { + Time = DateTime.UtcNow, + JobName = $"Job For Materialized {counter++}", + Length = 1000 + counter, + Thread = $"Coats Thread {counter}" + } + }); + } + } +} diff --git a/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Properties/AssemblyInfo.cs b/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..dc1b052d1 --- /dev/null +++ b/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Tango.Telemetry.Tester.IOT.CLI")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Tango.Telemetry.Tester.IOT.CLI")] +[assembly: AssemblyCopyright("Copyright © 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c891b2f7-e63a-40e2-b6ce-a22a69b4d86a")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Tango.Telemetry.Tester.IOT.CLI.csproj b/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Tango.Telemetry.Tester.IOT.CLI.csproj new file mode 100644 index 000000000..0449a67fe --- /dev/null +++ b/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/Tango.Telemetry.Tester.IOT.CLI.csproj @@ -0,0 +1,202 @@ + + + + + Debug + AnyCPU + {C891B2F7-E63A-40E2-B6CE-A22A69B4D86A} + Exe + Tango.Telemetry.Tester.IOT.CLI + Tango.Telemetry.Tester.IOT.CLI + v4.6.1 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\DotNetty.Buffers.0.4.6\lib\net45\DotNetty.Buffers.dll + + + ..\..\packages\DotNetty.Codecs.0.4.6\lib\net45\DotNetty.Codecs.dll + + + ..\..\packages\DotNetty.Codecs.Mqtt.0.4.6\lib\net45\DotNetty.Codecs.Mqtt.dll + + + ..\..\packages\DotNetty.Common.0.4.6\lib\net45\DotNetty.Common.dll + + + ..\..\packages\DotNetty.Handlers.0.4.6\lib\net45\DotNetty.Handlers.dll + + + ..\..\packages\DotNetty.Transport.0.4.6\lib\net45\DotNetty.Transport.dll + + + ..\..\packages\Microsoft.Azure.Amqp.2.0.6\lib\net45\Microsoft.Azure.Amqp.dll + + + ..\..\packages\Microsoft.Azure.Devices.Client.1.6.0\lib\net45\Microsoft.Azure.Devices.Client.dll + + + ..\..\packages\Microsoft.Azure.Devices.Shared.1.3.0\lib\net45\Microsoft.Azure.Devices.Shared.dll + + + ..\..\packages\Microsoft.Azure.KeyVault.Core.1.0.0\lib\net40\Microsoft.Azure.KeyVault.Core.dll + + + ..\..\packages\Microsoft.Data.Edm.5.6.4\lib\net40\Microsoft.Data.Edm.dll + + + ..\..\packages\Microsoft.Data.OData.5.6.4\lib\net40\Microsoft.Data.OData.dll + + + ..\..\packages\Microsoft.Data.Services.Client.5.6.4\lib\net40\Microsoft.Data.Services.Client.dll + + + ..\..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.1.1.0\lib\netstandard1.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + + ..\..\packages\Microsoft.Extensions.Logging.1.1.1\lib\netstandard1.1\Microsoft.Extensions.Logging.dll + + + ..\..\packages\Microsoft.Extensions.Logging.Abstractions.1.1.1\lib\netstandard1.1\Microsoft.Extensions.Logging.Abstractions.dll + + + ..\..\packages\EnterpriseLibrary.TransientFaultHandling.6.0.1304.0\lib\portable-net45+win+wp8\Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling.dll + + + ..\..\packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll + + + ..\..\packages\WindowsAzure.Storage.7.0.0\lib\net40\Microsoft.WindowsAzure.Storage.dll + + + ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + + + ..\..\packages\PCLCrypto.2.0.147\lib\net45\PCLCrypto.dll + + + ..\..\packages\PInvoke.BCrypt.0.3.2\lib\net40\PInvoke.BCrypt.dll + + + ..\..\packages\PInvoke.Kernel32.0.3.2\lib\net40\PInvoke.Kernel32.dll + + + ..\..\packages\PInvoke.NCrypt.0.3.2\lib\net40\PInvoke.NCrypt.dll + + + ..\..\packages\PInvoke.Windows.Core.0.3.2\lib\portable-net45+win+wpa81+MonoAndroid10+xamarinios10+MonoTouch10\PInvoke.Windows.Core.dll + + + + ..\..\packages\System.AppContext.4.3.0\lib\net46\System.AppContext.dll + + + + ..\..\packages\System.Console.4.3.0\lib\net46\System.Console.dll + + + + ..\..\packages\System.Diagnostics.DiagnosticSource.4.3.0\lib\net46\System.Diagnostics.DiagnosticSource.dll + + + ..\..\packages\System.Globalization.Calendars.4.3.0\lib\net46\System.Globalization.Calendars.dll + + + ..\..\packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll + + + + ..\..\packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll + + + ..\..\packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll + + + ..\..\packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll + + + ..\..\packages\System.Net.Http.4.3.0\lib\net46\System.Net.Http.dll + + + ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll + + + ..\..\packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll + + + + ..\..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll + + + ..\..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net461\System.Security.Cryptography.Algorithms.dll + + + ..\..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll + + + ..\..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll + + + ..\..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net461\System.Security.Cryptography.X509Certificates.dll + + + ..\..\packages\System.Spatial.5.6.4\lib\net40\System.Spatial.dll + + + + + + + + ..\..\packages\System.Xml.ReaderWriter.4.3.0\lib\net46\System.Xml.ReaderWriter.dll + + + ..\..\packages\Validation.2.2.8\lib\dotnet\Validation.dll + + + + + + + + + + + + + {f441feee-322a-4943-b566-110e12fd3b72} + Tango.BL + + + {a34ee0f0-649d-41c8-8489-b6f1cc6924ee} + Tango.Core + + + {af593663-d4e9-4a14-a3f2-fea57f30e9e6} + Tango.Telemetry + + + + \ No newline at end of file diff --git a/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/packages.config b/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/packages.config new file mode 100644 index 000000000..8dabdb1c2 --- /dev/null +++ b/Software/Visual_Studio/Utilities/Tango.Telemetry.Tester.IOT.CLI/packages.config @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/App.config b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/App.config new file mode 100644 index 000000000..c248eaa70 --- /dev/null +++ b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/App.config @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/DatabaseHelper.cs b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/DatabaseHelper.cs new file mode 100644 index 000000000..60791a5d8 --- /dev/null +++ b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/DatabaseHelper.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tango.TelemetryTester.CLI +{ + public class DatabaseHelper + { + public static void ClearTelemetryDatabase() + { + string appName = AppDomain.CurrentDomain.FriendlyName.Replace(".exe", ""); + string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Twine", "Tango", "Telemetry"); + string file = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Twine", "Tango", "Telemetry", Path.GetFileNameWithoutExtension(AppDomain.CurrentDomain.FriendlyName) + ".telemetry"); + string logFile = Path.Combine(path, appName + "-log.telemetry"); + string backup = file + ".bak"; + + try + { + if (File.Exists(file)) File.Delete(file); + if (File.Exists(backup)) File.Delete(backup); + if (File.Exists(logFile)) File.Delete(logFile); + Logger.LogWarning("Cleaned telemetry database for fresh test run."); + } + catch (Exception ex) + { + Logger.LogError($"Failed to clear telemetry database: {ex.Message}"); + } + } + } +} diff --git a/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Logger.cs b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Logger.cs new file mode 100644 index 000000000..8490ccfc6 --- /dev/null +++ b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Logger.cs @@ -0,0 +1,49 @@ +using System; + +namespace Tango.TelemetryTester.CLI +{ + public static class Logger + { + public static void LogInfo(String message) + { + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine(message); + } + + public static void LogWarning(String message) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(message); + } + + public static void LogError(String message) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(message); + } + + public static void LogSuccess(String message) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine(message); + } + + public static void LogVerbose(String message) + { + Console.ForegroundColor = ConsoleColor.Gray; + Console.WriteLine(message); + } + + public static void LogVerboseByLogManager(String message) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine(message); + } + + public static void LogTitle(string title) + { + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine($"\n===== {title} ====="); + } + } +} diff --git a/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockCheckpointsRecoveryClient.cs b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockCheckpointsRecoveryClient.cs new file mode 100644 index 000000000..82b7d739f --- /dev/null +++ b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockCheckpointsRecoveryClient.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Tango.Telemetry; + +namespace Tango.TelemetryTester.CLI +{ + public class MockCheckpointsRecoveryClient : ITelemetryCheckpointsRecoveryClient + { + public Task> GetCheckpointsBackup() => Task.FromResult(new List()); + public Task SaveCheckpointsBackup(List checkPoints) => Task.CompletedTask; + public void Dispose() { } + } +} diff --git a/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockDestinationWithFailure.cs b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockDestinationWithFailure.cs new file mode 100644 index 000000000..400dbf3ef --- /dev/null +++ b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockDestinationWithFailure.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Tango.Telemetry; + +namespace Tango.TelemetryTester.CLI +{ + public class MockDestinationWithFailure : ITelemetryDestination + { + public string Name { get; set; } + public List ReceivedPayloads { get; } = new List(); + public int FailCountRemaining { get; private set; } + public int TotalAttempts { get; private set; } = 0; + + public MockDestinationWithFailure(string name, int failCount) + { + Name = name; + FailCountRemaining = failCount; + } + + public IReadOnlyList SupportedSourceTypes => new[] + { + TelemetrySourceTypes.Streaming, + TelemetrySourceTypes.ExternalStorage, + TelemetrySourceTypes.PendingStorage + }; + + public Task IsAvailable() => Task.FromResult(true); + + public Task Publish(TelemetryPublishPackage package, List> properties) + { + TotalAttempts++; + if (FailCountRemaining-- > 0) + { + throw new Exception("Simulated publish failure"); + } + + var payload = package.ToPayload(); + ReceivedPayloads.Add(payload); + Logger.LogInfo($"Destination '{Name}' received payload: {payload}"); + return Task.CompletedTask; + } + + public void Dispose() { } + } +} diff --git a/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockHistorySource.cs b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockHistorySource.cs new file mode 100644 index 000000000..6ecd7e637 --- /dev/null +++ b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockHistorySource.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Tango.Telemetry; + +namespace Tango.TelemetryTester.CLI +{ + public class MockHistorySource : ITelemetryHistorySource + { + public string Name { get; } + + public int ProvidedCount { get; set; } + + public MockHistorySource(string name) + { + Name = name; + } + + public Task CanRequestHistory(DateTime from) => Task.FromResult(true); + + public Task> RequestHistory(DateTime from) + { + Logger.LogInfo($"[HistorySource] Providing historical telemetry at {DateTime.UtcNow}"); + + ProvidedCount++; + + return Task.FromResult>(new[] + { + new MockTelemetry { Time = DateTime.UtcNow.AddSeconds(-30) } + }); + } + + public void Dispose() { } + } +} diff --git a/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockStreamingSource.cs b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockStreamingSource.cs new file mode 100644 index 000000000..0ca058ec9 --- /dev/null +++ b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockStreamingSource.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using Tango.Telemetry; + +namespace Tango.TelemetryTester.CLI +{ + public class MockStreamingSource : ITelemetryStreamingSource + { + public string Name { get; } + public event EventHandler TelemetryAvailable; + private Timer _timer; + public int EmittedCount { get; private set; } + private bool _isStarted = false; + + public MockStreamingSource(string name) + { + Name = name; + } + + public void Start() + { + if (_isStarted) return; + _isStarted = true; + _timer = new Timer(SendTelemetry, null, 500, 1000); + } + + public void Stop() + { + if (!_isStarted) return; + _timer?.Dispose(); + _timer = null; + _isStarted = false; + } + + private void SendTelemetry(object state) + { + var telemetry = new MockTelemetry { Time = DateTime.UtcNow }; + EmittedCount++; + Logger.LogVerbose($"Emitting telemetry #{EmittedCount} from {Name}"); + TelemetryAvailable?.Invoke(this, new TelemetryAvailableEventArgs(telemetry, TelemetrySourceTypes.Streaming)); + } + + public void Dispose() => Stop(); + } +} diff --git a/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockTelemetry.cs b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockTelemetry.cs new file mode 100644 index 000000000..fb96e5819 --- /dev/null +++ b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/MockTelemetry.cs @@ -0,0 +1,21 @@ +using System; +using System.Text; +using Tango.Telemetry; + +namespace Tango.TelemetryTester.CLI +{ + public class MockTelemetry : ITelemetry + { + public DateTime Time { get; set; } + + public string ToJson(Newtonsoft.Json.Formatting format = Newtonsoft.Json.Formatting.None, bool flatten = true) + { + return $"{{\"time\": \"{Time:o}\"}}"; + } + + public byte[] ToBytes(Newtonsoft.Json.Formatting format = Newtonsoft.Json.Formatting.None, bool flatten = true) + { + return Encoding.UTF8.GetBytes(ToJson(format, flatten)); + } + } +} diff --git a/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Program.cs b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Program.cs new file mode 100644 index 000000000..1b985e35b --- /dev/null +++ b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Program.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Tango.BL.Enumerations; +using Tango.Logging; +using Tango.Telemetry; + +namespace Tango.TelemetryTester.CLI +{ + class Program + { + static void Main(string[] args) + { + Run().GetAwaiter().GetResult(); + } + + static async Task Run() + { + Logger.LogTitle("Telemetry Pipeline Test Starting..."); + DatabaseHelper.ClearTelemetryDatabase(); + + LogManager.Default.NewLog += (x, log) => + { + switch (log.Category) + { + case LogCategory.Info: + case LogCategory.Debug: + Logger.LogVerboseByLogManager(log.Message); + break; + case LogCategory.Warning: + Logger.LogWarning(log.Message); + break; + case LogCategory.Error: + case LogCategory.Critical: + Logger.LogError(log.Message); + break; + } + }; + + var config = new TelemetryPublisherConfiguration + { + SerialNumber = "TEST-MACHINE", + MachineType = MachineTypes.TS1800, + Environment = "DEV", + PendingStorageCheckInterval = TimeSpan.FromSeconds(5), + HistorySourcesRequestInterval = TimeSpan.FromSeconds(5) + }; + + var recoveryClient = new MockCheckpointsRecoveryClient(); + var publisher = new TelemetryPublisher(config, recoveryClient); + + var mockStreamingSource = new MockStreamingSource("MockStreamingSource1"); + var mockHistorySource = new MockHistorySource("MockHistorySource1"); + var destination1 = new MockDestinationWithFailure("MockDestination", failCount: 2); + var destination2 = new MockDestinationWithFailure("FastDestination", failCount: 0); + var destination3 = new MockDestinationWithFailure("HistoryOnlyDestination", failCount: 0); + + publisher.RegisterSource(mockStreamingSource); + publisher.RegisterSource(mockHistorySource); + publisher.RegisterDestination(destination1); + publisher.RegisterDestination(destination2); + publisher.RegisterDestination(destination3); + + var results = new List(); + var publishCount = 0; + var failureCount = 0; + + publisher.PackagePublished += (s, e) => + { + publishCount++; + var msg = $"SUCCESS: published telemetry to {e.Destination.Name}"; + results.Add(msg); + Logger.LogSuccess(msg); + }; + + publisher.PublishPackageFailed += (s, e) => + { + failureCount++; + var msg = $"FAILURE: failed to publish telemetry to {e.Destination.Name} - {e.Exception.Message}"; + results.Add(msg); + Logger.LogError(msg); + }; + + await publisher.Start(); + await Task.Delay(7000); + await publisher.Stop(); + + int pendingCount = publisher.StorageManager.GetPendingTelemetriesCount(); + + Logger.LogTitle("TEST REPORT"); + Logger.LogInfo($"Total telemetry generated by streaming source: {mockStreamingSource.EmittedCount}"); + Logger.LogInfo($"Total telemetry generated by history source: {mockHistorySource.ProvidedCount}"); + Logger.LogInfo($"Total failures (intentional): {failureCount}"); + Logger.LogInfo($"Total payloads received by MockDestination: {destination1.ReceivedPayloads.Count}"); + Logger.LogInfo($"Total payloads received by FastDestination: {destination2.ReceivedPayloads.Count}"); + Logger.LogInfo($"Total payloads received by HistoryOnlyDestination: {destination3.ReceivedPayloads.Count}"); + Logger.LogInfo($"Total telemetries published: {publishCount}"); + + int totalExpectedPublishes = (mockStreamingSource.EmittedCount + mockHistorySource.ProvidedCount) * 3; + bool allEmittedPublished = publishCount == totalExpectedPublishes; + bool allPublishedReceived = publishCount == destination1.ReceivedPayloads.Count + destination2.ReceivedPayloads.Count + destination3.ReceivedPayloads.Count; + bool retriesOccurred = destination1.TotalAttempts > destination1.ReceivedPayloads.Count; + bool noPendingLeft = pendingCount == 0; + + Logger.LogTitle("TEST VERDICT"); + Logger.LogInfo(allEmittedPublished ? "PASS: Emitted telemetry all published" : "FAIL: Emitted telemetry not fully published"); + Logger.LogInfo(allPublishedReceived ? "PASS: All published telemetry received by destinations" : "FAIL: Some published telemetry was not received"); + Logger.LogInfo(retriesOccurred ? "PASS: Retry logic triggered and succeeded" : "FAIL: Retry logic did not activate as expected"); + Logger.LogInfo(noPendingLeft ? "PASS: Pending storage clean after test" : $"FAIL: {pendingCount} telemetry still pending in storage"); + + Logger.LogTitle("DETAILED EVENTS"); + foreach (var line in results) + Logger.LogInfo(line); + + Logger.LogSuccess("\nTest complete."); + Console.ReadKey(); + } + } +} diff --git a/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Properties/AssemblyInfo.cs b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..e4666e75a --- /dev/null +++ b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Tango.TelemetryTester.CLI")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Tango.TelemetryTester.CLI")] +[assembly: AssemblyCopyright("Copyright © 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("db3a1470-994b-46df-9be8-f905cdb97a3a")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Tango.TelemetryTester.CLI.csproj b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Tango.TelemetryTester.CLI.csproj new file mode 100644 index 000000000..cc4ba9298 --- /dev/null +++ b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Tango.TelemetryTester.CLI.csproj @@ -0,0 +1,83 @@ + + + + + Debug + AnyCPU + {DB3A1470-994B-46DF-9BE8-F905CDB97A3A} + Exe + Tango.TelemetryTester.CLI + Tango.TelemetryTester.CLI + v4.6.1 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {f441feee-322a-4943-b566-110e12fd3b72} + Tango.BL + + + {a34ee0f0-649d-41c8-8489-b6f1cc6924ee} + Tango.Core + + + {bc932dbd-7cdb-488c-99e4-f02cf441f55e} + Tango.Logging + + + {af593663-d4e9-4a14-a3f2-fea57f30e9e6} + Tango.Telemetry + + + + \ No newline at end of file diff --git a/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Verifications List.md b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Verifications List.md new file mode 100644 index 000000000..90362d659 --- /dev/null +++ b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/Verifications List.md @@ -0,0 +1,90 @@ +### ✅ **Basic Functional Verifications** + +1. **Source Trigger Verification** + + * Log a message every time `MockStreamingSource` generates a telemetry. + * Count how many times `TelemetryAvailable` was raised vs. published. + +2. **Destination Receipt Check** + + * Confirm each destination receives telemetry that matches the `ToPayload()` value. + * Add internal counters per destination to confirm delivery. + +3. **Package Identity Tracking** + + * Assign a unique ID to each telemetry object and verify that: + + * It travels all the way to the destination. + * It is not duplicated or lost. + +--- + +### 🔁 **Retry and Backoff Testing** + +4. **Destination Retry Logic** + + * Simulate a failure in `MockDestination.Publish()` every Nth call. + * Verify that retries are scheduled with exponential backoff. + +5. **Pending Storage Write/Read** + + * Cause all destinations to fail. + * Confirm that telemetry is persisted to `TelemetryLiteDBStorageManager`. + * Manually inspect `PendingTelemetries.Count` after the run. + +--- + +### 🕒 **Time & Latency Metrics** + +6. **Elapsed Time Per Publish** + + * Log how long it takes from package creation to successful publish. + +7. **Latency Bucketing** + + * Track telemetry latency buckets (e.g., `<1s`, `1–5s`, `>5s`) and print histogram. + +--- + +### 📦 **Historical and Pending Telemetry** + +8. **History Source Verification** + + * Confirm `MockHistorySource.RequestHistory()` is triggered periodically. + * Verify telemetry from it is published and updated in checkpoints. + +9. **Checkpoint Verification** + + * Track when and how often checkpoints are updated. + * Ensure they persist across runs using `TelemetryLiteDBStorageManager`. + +--- + +### 🛠️ **Fault Injection and Resilience** + +10. **Out-of-Order Timestamps** + + * Send telemetry with past/future timestamps and check ordering in publish results. + +11. **Multiple Concurrent Sources** + + * Add 2–3 `MockStreamingSource` instances and confirm the system handles interleaved input. + +12. **Slow Publisher Simulation** + + * Simulate slow or blocking destination publish methods. + * Confirm queue doesn’t overflow and telemetry is not lost. + +--- + +### 🧪 **Extended Diagnostic Reporting** + +13. **Final Report Enhancements** + + * Total telemetry generated, published, failed, retried, and persisted. + * Telemetry per source/destination. + * Count of `TelemetryAvailable` vs `Publish()`. + +--- + +Would you like to start with a few of these now? I can help you implement fault injection, persistence inspection, or anything else you choose. diff --git a/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/packages.config b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/packages.config new file mode 100644 index 000000000..7ee8c1052 --- /dev/null +++ b/Software/Visual_Studio/Utilities/Tango.TelemetryTester.CLI/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file -- cgit v1.3.1