// 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();
}
}
}
}