// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; namespace EpicGames.Core { /// /// Interface for a trace span /// public interface ITraceSpan : IDisposable { /// /// Adds additional metadata to this scope /// /// Name of the key /// Value for this metadata void AddMetadata(string name, string value); } /// /// Sink for tracing information /// public interface ITraceSink { /// /// Create a new trace span /// /// Name of the operation /// Resource that the operation is being performed on /// Name of the service /// New span instance ITraceSpan Create(string operation, string? resource, string? service); /// /// Flush all the current trace data /// void Flush(); } /// /// Default implementation of ITraceSink. Writes trace information to a JSON file on application exit if the UE_TELEMETRY_DIR environment variable is set. /// public class JsonTraceSink : ITraceSink { class TraceSpanImpl : ITraceSpan { public string Name { get; } public string? Resource { get; } public string? Service { get; } public DateTimeOffset StartTime { get; } public DateTimeOffset? FinishTime { get; private set; } public Dictionary Metadata { get; } = []; public TraceSpanImpl(string name, string? resource, string? service) { Name = name; Resource = resource; Service = service; StartTime = DateTimeOffset.Now; } public void AddMetadata(string name, string value) { Metadata[name] = value; } public void Dispose() { if (FinishTime == null) { FinishTime = DateTimeOffset.Now; } } } /// /// Output directory for telemetry info /// readonly DirectoryReference _telemetryDir; /// /// The current scope provider /// readonly List _spans = []; /// /// Constructor /// /// Directory to store telemetry files public JsonTraceSink(DirectoryReference telemetryDir) { _telemetryDir = telemetryDir; } /// /// Creates a scope using the current provider /// public ITraceSpan Create(string name, string? resource = null, string? service = null) { TraceSpanImpl span = new TraceSpanImpl(name, resource, service); _spans.Add(span); return span; } /// /// Saves all the scope information to a file /// public void Flush() { FileReference file; using (Process process = Process.GetCurrentProcess()) { DirectoryReference.CreateDirectory(_telemetryDir); string fileName = String.Format("{0}.{1}.{2}.json", Path.GetFileName(Assembly.GetEntryAssembly()!.Location), process.Id, process.StartTime.Ticks); file = FileReference.Combine(_telemetryDir, fileName); } using (JsonWriter writer = new JsonWriter(file)) { writer.WriteObjectStart(); writer.WriteArrayStart("Spans"); foreach (TraceSpanImpl span in _spans) { if (span.FinishTime != null) { writer.WriteObjectStart(); writer.WriteValue("Name", span.Name); if (span.Resource != null) { writer.WriteValue("Resource", span.Resource); } if (span.Service != null) { writer.WriteValue("Service", span.Service); } writer.WriteValue("StartTime", span.StartTime.ToString("o", CultureInfo.InvariantCulture)); writer.WriteValue("FinishTime", span.FinishTime.Value.ToString("o", CultureInfo.InvariantCulture)); writer.WriteObjectStart("Metadata"); foreach (KeyValuePair pair in span.Metadata) { writer.WriteValue(pair.Key, pair.Value); } writer.WriteObjectEnd(); writer.WriteObjectEnd(); } } writer.WriteArrayEnd(); writer.WriteObjectEnd(); } } } /// /// Methods for creating ITraceScope instances /// public static class TraceSpan { class CombinedTraceSpan : ITraceSpan { readonly ITraceSpan[] _spans; public CombinedTraceSpan(ITraceSpan[] spans) { _spans = spans; } public void AddMetadata(string name, string value) { foreach(ITraceSpan span in _spans) { span.AddMetadata(name, value); } } public void Dispose() { foreach (ITraceSpan span in _spans) { span.Dispose(); } } } /// /// The sinks to use /// static readonly List s_sinks = GetDefaultSinks(); /// /// Build a list of default sinks /// /// static List GetDefaultSinks() { List sinks = []; string? telemetryDir = Environment.GetEnvironmentVariable("UE_TELEMETRY_DIR"); if (telemetryDir != null) { sinks.Add(new JsonTraceSink(new DirectoryReference(telemetryDir))); } return sinks; } /// /// Adds a new sink /// /// The sink to add public static void AddSink(ITraceSink sink) { s_sinks.Add(sink); } /// /// Remove a sink from the current list /// /// The sink to remove public static void RemoveSink(ITraceSink sink) { s_sinks.Remove(sink); } /// /// Creates a scope using the current provider /// public static ITraceSpan Create(string operation, string? resource = null, string? service = null) { if (s_sinks.Count == 0) { return new CombinedTraceSpan([]); } else if (s_sinks.Count == 1) { return s_sinks[0].Create(operation, resource, service); } else { return new CombinedTraceSpan([.. s_sinks.ConvertAll(x => x.Create(operation, resource, service))]); } } /// /// Saves all the scope information to a file /// public static void Flush() { foreach (ITraceSink sink in s_sinks) { sink.Flush(); } } } }