26
loading...
This website collects cookies to deliver better user experience
class Program
{
private static async Task Main(string[] args)
{
using IHost host = CreatHostBuilder(args).Build();
await host.RunAsync();
}
static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((configuration) =>
{
configuration.AddEnvironmentVariables("TREX")
; // can add more sources such as command line
})
.ConfigureServices(c =>
{
c.AddSingleton<MetricCollection>(); // This is where we will keep all metrics state. hence singleton
c.AddHostedService<PrometheusExporter>(); // exposes MetricCollection
c.AddHostedService<TRexPoller>(); // periodically GETs status and updates MetricCollection
});
}
TRexPoller
and PrometheusExporter
. Writing both is trivial and we won’t spend much time on the code there. Feel free to check it out on GitHub. The point to make here is it has never been easier to focus on business logic and leave heavy lifting to respective NuGet packages.// generated code looks like this. A set of POCOs with each property decorated with JsonProperty that maps to api response
public partial class Gpu
{
[JsonProperty("device_id")]
public int DeviceId { get; set; }
[JsonProperty("hashrate")]
public int Hashrate { get; set; }
[JsonProperty("hashrate_day")]
public int HashrateDay { get; set; }
[JsonProperty("hashrate_hour")]
public int HashrateHour { get; set; }
...
}
// example taken from https://github.com/prometheus-net/prometheus-net#quick-start
private static readonly Counter ProcessedJobCount = Metrics
.CreateCounter("myapp_jobs_processed_total", "Number of processed jobs.");
...
ProcessJob();
ProcessedJobCount.Inc();
[AddInstrumentation("gpus")] // the first attribute prompts the generator to loop through the properties and search for metrics
public partial class Gpu
{
[JsonProperty("device_id")]
public int DeviceId { get; set; }
[JsonProperty("hashrate")]
/*
* the second attribute controls which type the metric will have as well as what labels we want to store with it.
* In this example, it's a Gauge with gpu_id, vendor and name being labels for grouping in Prometheus
*/
[Metric("Gauge", "gpu_id", "vendor", "name")]
public int Hashrate { get; set; }
[JsonProperty("hashrate_day")]
[Metric("Gauge", "gpu_id", "vendor", "name")]
public int HashrateDay { get; set; }
[JsonProperty("hashrate_hour")]
[Metric("Gauge", "gpu_id", "vendor", "name")]
public int HashrateHour { get; set; }
ClassDeclarationSyntax
and looks for well-known attributes:public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax cds && cds.AttributeLists
.SelectMany(al => al.Attributes)
.Any(a => (a.Name as IdentifierNameSyntax)?.Identifier.ValueText == "AddInstrumentation"))
{
ClassesToProcess.Add(cds);
}
}
Collector
objects.var text = new StringBuilder(@"public static Dictionary<string, Collector> GetMetrics(string prefix)
{
var result = new Dictionary<string, Collector>
{").AppendLine();
foreach (PropertyDeclarationSyntax p in properties)
{
var jsonPropertyAttr = p.GetAttr("JsonProperty");
var metricAttr = p.GetAttr("Metric");
if (metricAttr == null) continue;
var propName = jsonPropertyAttr.GetFirstParameterValue();
var metricName = metricAttr.GetFirstParameterValue(); // determine metric type
if (metricAttr.ArgumentList.Arguments.Count > 1)
{
var labels = metricAttr.GetTailParameterValues(); // if we have extra labels to process - here's our chance
text.AppendLine(
$"{{$\"{{prefix}}{attrPrefix}_{propName}\", Metrics.Create{metricName}($\"{{prefix}}{attrPrefix}_{propName}\", \"{propName}\", {commonLabels}, {labels}) }},");
}
else
{
text.AppendLine(
$"{{$\"{{prefix}}{attrPrefix}_{propName}\", Metrics.Create{metricName}($\"{{prefix}}{attrPrefix}_{propName}\", \"{propName}\", {commonLabels}) }},");
}
}
text.AppendLine(@"};
return result;
}");
private StringBuilder UpdateMetrics(List<MemberDeclarationSyntax> properties, SyntaxToken classToProcess, string attrPrefix)
{
var text = new StringBuilder($"public static void UpdateMetrics(string prefix, Dictionary<string, Collector> metrics, {classToProcess} data, string host, string slot, string algo, List<string> extraLabels = null) {{");
text.AppendLine();
text.AppendLine(@"if(extraLabels == null) {
extraLabels = new List<string> {host, slot, algo};
}
else {
extraLabels.Insert(0, algo);
extraLabels.Insert(0, slot);
extraLabels.Insert(0, host);
}");
foreach (PropertyDeclarationSyntax p in properties)
{
var jsonPropertyAttr = p.GetAttr("JsonProperty");
var metricAttr = p.GetAttr("Metric");
if (metricAttr == null) continue;
var propName = jsonPropertyAttr.GetFirstParameterValue();
var metricName = metricAttr.GetFirstParameterValue();
var newValue = $"data.{p.Identifier.ValueText}";
text.Append(
$"(metrics[$\"{{prefix}}{attrPrefix}_{propName}\"] as {metricName}).WithLabels(extraLabels.ToArray())");
switch (metricName)
{
case "Counter": text.AppendLine($".IncTo({newValue});"); break;
case "Gauge": text.AppendLine($".Set({newValue});"); break;
}
}
text.AppendLine("}").AppendLine();
return text;
}
internal class MetricCollection
{
private readonly Dictionary<string, Collector> _metrics;
private readonly string _prefix;
private readonly string _host;
public MetricCollection(IConfiguration configuration)
{
_prefix = configuration.GetValue<string>("exporterPrefix", "trex");
_metrics = new Dictionary<string, Collector>();
// this is where declaring particl classes and generating extra methods makes for seamless development experience
foreach (var (key, value) in TRexResponse.GetMetrics(_prefix)) _metrics.Add(key, value);
foreach (var (key, value) in DualStat.GetMetrics(_prefix)) _metrics.Add(key, value);
foreach (var (key, value) in Gpu.GetMetrics(_prefix)) _metrics.Add(key, value);
foreach (var (key, value) in Shares.GetMetrics(_prefix)) _metrics.Add(key, value);
}
public void Update(TRexResponse data)
{
TRexResponse.UpdateMetrics(_prefix, _metrics, data, _host, "main", data.Algorithm);
DualStat.UpdateMetrics(_prefix, _metrics, data.DualStat, _host, "dual", data.DualStat.Algorithm);
foreach (var dataGpu in data.Gpus)
{
Gpu.UpdateMetrics(_prefix, _metrics, dataGpu, _host, "main", data.Algorithm, new List<string>
{
dataGpu.DeviceId.ToString(),
dataGpu.Vendor,
dataGpu.Name
});
Shares.UpdateMetrics(_prefix, _metrics, dataGpu.Shares, _host, "main", data.Algorithm, new List<string>
{
dataGpu.GpuId.ToString(),
dataGpu.Vendor,
dataGpu.Name
});
}
}
}
public partial class Shares {
public static Dictionary<string, Collector> GetMetrics(string prefix)
{
var result = new Dictionary<string, Collector>
{
{$"{prefix}_shares_accepted_count", Metrics.CreateCounter($"{prefix}_shares_accepted_count", "accepted_count", "host", "slot", "algo", "gpu_id", "vendor", "name") },
{$"{prefix}_shares_invalid_count", Metrics.CreateCounter($"{prefix}_shares_invalid_count", "invalid_count", "host", "slot", "algo", "gpu_id", "vendor", "name") },
{$"{prefix}_shares_last_share_diff", Metrics.CreateGauge($"{prefix}_shares_last_share_diff", "last_share_diff", "host", "slot", "algo", "gpu_id", "vendor", "name") },
...
};
return result;
}
public static void UpdateMetrics(string prefix, Dictionary<string, Collector> metrics, Shares data, string host, string slot, string algo, List<string> extraLabels = null) {
if(extraLabels == null) {
extraLabels = new List<string> {host, slot, algo};
}
else {
extraLabels.Insert(0, algo);
extraLabels.Insert(0, slot);
extraLabels.Insert(0, host);
}
(metrics[$"{prefix}_shares_accepted_count"] as Counter).WithLabels(extraLabels.ToArray()).IncTo(data.AcceptedCount);
(metrics[$"{prefix}_shares_invalid_count"] as Counter).WithLabels(extraLabels.ToArray()).IncTo(data.InvalidCount);
(metrics[$"{prefix}_shares_last_share_diff"] as Gauge).WithLabels(extraLabels.ToArray()).Set(data.LastShareDiff);
...
}
}