Code/MetricsSystem.cs
using System.IO;
using System.IO.Compression;
using System.Net.Http;
using System.Text.Json;
public class MetricsSystem : GameObjectSystem
{
[ConVar( "manic_metrics_enabled", Saved = true )]
public static bool Enabled { get; set; } = true;
[ConVar( "manic_metrics_endpoint", Saved = true )]
public static string URL { get; set; } = "https://ggg.sethp.cc/api/v1/import";
[ConVar( "manic_metrics_cooldown", Saved = true )]
public static int MetricsDelay { get; set; } = 5;
[ConVar( "manic_metrics_logging", Saved = true )]
public static bool DebugLogging { get; set; } = false;
TimeUntil canSendMetrics = MetricsDelay;
public Dictionary<string, string> GzipHeader = new()
{
{"Content-Encoding", "gzip"}
};
public MetricsSystem( Scene scene ) : base( scene )
{
// Only the host send off metrics
if ( !Networking.IsHost )
return;
if ( !Http.IsAllowed( new System.Uri( URL ) ) )
throw new Exception( $"cannot send metrics to there, not allowed: {URL}" );
// Listen( Stage.SceneLoaded, 1, DebugMetrics, "DebugMetrics" );
Listen( Stage.FinishFixedUpdate, 1, SendMetrics, "SendMetrics" );
}
private readonly List<MetricStreamEntry> Queue = new();
void DebugMetrics()
{
Log.Info( $"Prepping to send debug game metrics to: {URL}" );
var m = new MetricStreamEntry()
{
Metric = new SampleMetric( "debug" )
};
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
m.Backfill( now - 1000 * 60 * 2, 2 );
m.Backfill( now - 1000 * 60 * 1, 6 );
m.Backfill( now - 1000 * 20, 8 );
m.Add( 10 );
Queue.Add( m );
m = new MetricStreamEntry()
{
Metric = new SampleMetric( "debug2" )
};
m.Backfill( now - 1000 * 80 * 2, 1 );
m.Backfill( now - 1000 * 40 * 1, 20 );
m.Backfill( now - 1000 * 10, 40 );
m.Add( 55 );
Queue.Add( m );
}
void SendMetrics()
{
if ( Game.ActiveScene.IsEditor || !Enabled )
return;
if ( canSendMetrics && Queue.Count != 0 )
{
canSendMetrics = MetricsDelay;
FlushMetrics();
}
}
static readonly bool isUsingGzip = false;
async void FlushMetrics()
{
var body = string.Join( "\n", Queue.Select( mse => JsonSerializer.Serialize( mse ) ) );
if ( DebugLogging )
{
Log.Warning( $"==== Metrics Payload ====" );
Log.Warning( body );
}
HttpContent content;
Dictionary<string, string> headers = null;
if ( isUsingGzip )
{
byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes( body );
using var outputStream = new MemoryStream();
using ( var gZipStream = new GZipStream( outputStream, CompressionMode.Compress ) )
gZipStream.Write( inputBytes, 0, inputBytes.Length );
headers = GzipHeader;
content = new ByteArrayContent( outputStream.ToArray() );
}
else
{
content = new StringContent( body );
}
var url = URL
+ $"?extra_label=editor={Game.IsEditor}"
+ $"&extra_label=ident={Game.Ident}"
+ $"&extra_label=hoststeamid={Game.SteamId}"
+ $"&extra_label=scene={Game.ActiveScene.Name}"
+ $"&extra_label=party={Connection.Local.PartyId}";
if ( Enabled )
{
var response = await Http.RequestAsync( url, "POST", content, headers );
if ( !response.IsSuccessStatusCode )
Log.Info( $"Metrics Upload Issue: {response.StatusCode} {response}" );
if ( DebugLogging )
Log.Info( $"Metrics Upload Status: {response.StatusCode} {response}" );
}
Queue.Clear();
}
private static MetricStreamEntry GetEntry( IMetric metric )
{
var system = Game.ActiveScene.GetSystem<MetricsSystem>();
system.canSendMetrics = MetricsDelay; // re-up flush delay
var streamEntry = system.Queue.FirstOrDefault( mse => mse.Metric.GetHashCode().Equals( metric.GetHashCode() ) );
if ( streamEntry == null )
{
if ( DebugLogging )
Log.Info( $"adding '{metric.Name}' to the send queue" );
streamEntry = new MetricStreamEntry() { Metric = metric };
system.Queue.Add( streamEntry );
}
return streamEntry;
}
#region Commands
[ConCmd( "manic_metrics", Help = "print queued metrics" )]
public static void Kill()
{
var system = Game.ActiveScene.GetSystem<MetricsSystem>();
if ( system.Queue.Count == 0 )
{
Log.Info( "Metrics Queue is empty" );
return;
}
Log.Info( $"==== Metrics in Queue: {system.Queue.Count} ====" );
foreach ( var mqe in system.Queue )
Log.Info( mqe );
}
#endregion
#region Utilities
public static void Increment( IMetric metric )
{
if ( !Networking.IsHost ) return;
GetEntry( metric ).Increment();
}
public static void Set( IMetric metric, float value )
{
if ( !Networking.IsHost ) return;
GetEntry( metric ).Set( value );
}
#endregion
}