// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Text; using System.Text.Json; using System.Threading.Channels; using System.Threading.Tasks; using System.Web; using Microsoft.Extensions.Logging; namespace UnrealGameSync { /// /// Interface for a telemetry sink /// public interface ITelemetrySink : IDisposable { /// /// Sends a telemetry event with the given information /// /// Name of the event /// Arbitrary object to include in the payload void SendEvent(string eventName, object attributes); } /// /// Telemetry sink that discards all events /// public sealed class NullTelemetrySink : ITelemetrySink { /// public void Dispose() { } /// public void SendEvent(string eventName, object attributes) { } } /// /// Epic internal telemetry sink using the data router /// public sealed class EpicTelemetrySink : ITelemetrySink { readonly string _url; readonly ILogger _logger; readonly HttpClient _httpClient = new HttpClient(); readonly Channel _channel = Channel.CreateUnbounded(new UnboundedChannelOptions()); readonly Task _backgroundTask; /// /// Constructor /// public EpicTelemetrySink(string url, ILogger logger) { _url = url; _logger = logger; logger.LogInformation("Posting to URL: {Url}", url); _backgroundTask = Task.Run(WriteEventsAsync); } /// public void Dispose() { _channel.Writer.TryComplete(); try { _backgroundTask.Wait(); } catch (OperationCanceledException) { } _httpClient.Dispose(); } /// public void SendEvent(string eventName, object attributes) { string attributesText = JsonSerializer.Serialize(attributes); if (attributesText[0] != '{') { throw new Exception("Expected event data with named properties"); } string eventText = attributesText.Insert(1, String.Format("\"EventName\":\"{0}\",", HttpUtility.JavaScriptStringEncode(eventName))); _channel.Writer.TryWrite(eventText); } /// /// Synchronously sends a telemetry event /// async Task WriteEventsAsync() { string version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0"; while (await _channel.Reader.WaitToReadAsync()) { try { // Generate the content for this event List events = await _channel.Reader.ReadAllAsync().ToListAsync(); if (events.Count == 0) { continue; } // Print all the events we're sending foreach (string evt in events) { _logger.LogInformation("Sending Event: {Event}", evt); } // Convert the content to UTF8 string contentText = String.Format("{{\"Events\":[{0}]}}", String.Join(",", events)); byte[] content = Encoding.UTF8.GetBytes(contentText); // Post the event data using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, _url)) { request.Headers.UserAgent.Add(new ProductInfoHeaderValue("UnrealGameSync", version)); request.Content = new ByteArrayContent(content); request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); using (HttpResponseMessage response = await _httpClient.SendAsync(request)) { if (response.IsSuccessStatusCode) { _logger.LogInformation("Response: {StatusCode}", (int)response.StatusCode); } else { string responseContent = await response.Content.ReadAsStringAsync(); _logger.LogError("Unable to send telemetry data to server ({Code}): {Message}", response.StatusCode, responseContent); } } } } catch (Exception ex) { _logger.LogError(ex, "Exception while attempting to send event"); } } } } /// /// Global telemetry static class /// public static class UgsTelemetry { /// /// The current telemetry provider /// public static ITelemetrySink? ActiveSink { get; set; } /// /// Sends a telemetry event with the given information /// /// Name of the event /// Arbitrary object to include in the payload public static void SendEvent(string eventName, object attributes) { ActiveSink?.SendEvent(eventName, attributes); } } }