Skip to content

Commit 98c15d9

Browse files
committed
added pushgateway support. refactored MetricServer and tester.fixes prometheus-net#19
1 parent e729dbb commit 98c15d9

11 files changed

+330
-39
lines changed

README.md

+9
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@ var metricServer = new MetricServer(port: 1234);
9191
metricServer.Start();
9292
```
9393

94+
## Pushgateway support
95+
96+
Metrics can be posted to a Pushgateway server over HTTP.
97+
98+
```csharp
99+
var metricServer = new MetricPusher(endpoint: "http://pushgateway.example.org:9091/metrics", job: "some_job");
100+
metricServer.Start();
101+
```
102+
94103
## Unit testing
95104
For simple usage the API uses static classes, which - in unit tests - can cause errors like this: "A collector with name '<NAME>' has already been registered!"
96105

prometheus-net/IMetricServer.cs

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reactive.Concurrency;
5+
using System.Text;
6+
7+
namespace Prometheus
8+
{
9+
public interface IMetricServer
10+
{
11+
void Start(IScheduler scheduler = null);
12+
void Stop();
13+
}
14+
}

prometheus-net/MetricHandler.cs

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reactive.Concurrency;
5+
using System.Text;
6+
using Prometheus.Advanced;
7+
8+
namespace Prometheus
9+
{
10+
public abstract class MetricHandler : IMetricServer
11+
{
12+
protected readonly ICollectorRegistry _registry;
13+
private IDisposable _schedulerDelegate;
14+
15+
protected MetricHandler(IEnumerable<IOnDemandCollector> standardCollectors = null,
16+
ICollectorRegistry registry = null)
17+
{
18+
_registry = registry ?? DefaultCollectorRegistry.Instance;
19+
if (_registry == DefaultCollectorRegistry.Instance)
20+
{
21+
// Default to DotNetStatsCollector if none specified
22+
// For no collectors, pass an empty collection
23+
if (standardCollectors == null)
24+
standardCollectors = new[] { new DotNetStatsCollector() };
25+
26+
DefaultCollectorRegistry.Instance.RegisterOnDemandCollectors(standardCollectors);
27+
}
28+
}
29+
30+
public void Start(IScheduler scheduler = null)
31+
{
32+
_schedulerDelegate = StartLoop(scheduler ?? Scheduler.Default);
33+
}
34+
35+
public void Stop()
36+
{
37+
if (_schedulerDelegate != null) _schedulerDelegate.Dispose();
38+
StopInner();
39+
}
40+
41+
protected virtual void StopInner()
42+
{
43+
44+
}
45+
46+
protected abstract IDisposable StartLoop(IScheduler scheduler);
47+
}
48+
}

prometheus-net/MetricPusher.cs

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Net;
7+
using System.Reactive.Concurrency;
8+
using System.Text;
9+
using System.Threading;
10+
using Prometheus.Advanced;
11+
12+
namespace Prometheus
13+
{
14+
public class MetricPusher : MetricHandler
15+
{
16+
private const string ContentType = "text/plain; version=0.0.4";
17+
private readonly TimeSpan _schedulerInterval;
18+
private readonly Uri _endpoint;
19+
private readonly object _syncObj = new object();
20+
21+
public MetricPusher(string endpoint, string job, string instance = null, long intervalMilliseconds = 1000, IEnumerable<Tuple<string, string>> additionalLabels = null, IEnumerable<IOnDemandCollector> standardCollectors = null, ICollectorRegistry registry = null) : base(standardCollectors, registry)
22+
{
23+
if (string.IsNullOrEmpty(endpoint))
24+
{
25+
throw new ArgumentNullException("endpoint");
26+
}
27+
if (string.IsNullOrEmpty(job))
28+
{
29+
throw new ArgumentNullException("job");
30+
}
31+
if (intervalMilliseconds <= 0)
32+
{
33+
throw new ArgumentException("Interval must be greater than zero", "intervalMilliseconds");
34+
}
35+
StringBuilder sb = new StringBuilder(string.Format("{0}/job/{1}", endpoint.TrimEnd('/'), job));
36+
if (!string.IsNullOrEmpty(instance))
37+
{
38+
sb.AppendFormat("/instance/{0}", instance);
39+
}
40+
if (additionalLabels != null)
41+
{
42+
foreach (var pair in additionalLabels)
43+
{
44+
if (pair == null || string.IsNullOrEmpty(pair.Item1) || string.IsNullOrEmpty(pair.Item2))
45+
{
46+
Trace.WriteLine("Ignoring invalid label set");
47+
continue;
48+
}
49+
sb.AppendFormat("/{0}/{1}", pair.Item1, pair.Item2);
50+
}
51+
}
52+
if (!Uri.TryCreate(sb.ToString(), UriKind.Absolute, out _endpoint))
53+
{
54+
throw new ArgumentException("Endpoint must be a valid url", "endpoint");
55+
}
56+
57+
_schedulerInterval = TimeSpan.FromMilliseconds(intervalMilliseconds);
58+
}
59+
60+
protected override IDisposable StartLoop(IScheduler scheduler)
61+
{
62+
return scheduler.SchedulePeriodic(_schedulerInterval, SendMetrics);
63+
}
64+
65+
private void SendMetrics()
66+
{
67+
if (Monitor.TryEnter(_syncObj))
68+
{
69+
try
70+
{
71+
using (var stream = new MemoryStream())
72+
{
73+
ScrapeHandler.ProcessScrapeRequest(_registry.CollectAll(), ContentType, stream);
74+
using (var client = new WebClient())
75+
{
76+
client.UploadData(_endpoint, "POST", stream.ToArray());
77+
}
78+
}
79+
}
80+
catch (Exception e)
81+
{
82+
Trace.WriteLine(string.Format("Exception in send metrics: {0}", e));
83+
}
84+
finally
85+
{
86+
Monitor.Exit(_syncObj);
87+
}
88+
}
89+
}
90+
}
91+
}

prometheus-net/MetricServer.cs

+6-30
Original file line numberDiff line numberDiff line change
@@ -7,49 +7,25 @@
77

88
namespace Prometheus
99
{
10-
public interface IMetricServer
11-
{
12-
void Start(IScheduler scheduler = null);
13-
void Stop();
14-
}
15-
16-
public class MetricServer : IMetricServer
10+
public class MetricServer : MetricHandler
1711
{
1812
readonly HttpListener _httpListener = new HttpListener();
19-
readonly ICollectorRegistry _registry;
20-
private IDisposable _schedulerDelegate;
2113

2214
public MetricServer(int port, IEnumerable<IOnDemandCollector> standardCollectors = null, string url = "metrics/", ICollectorRegistry registry = null, bool useHttps = false) : this("+", port, standardCollectors, url, registry, useHttps)
2315
{
2416
}
2517

26-
public MetricServer(string hostname, int port, IEnumerable<IOnDemandCollector> standardCollectors = null, string url = "metrics/", ICollectorRegistry registry = null, bool useHttps = false)
18+
public MetricServer(string hostname, int port, IEnumerable<IOnDemandCollector> standardCollectors = null, string url = "metrics/", ICollectorRegistry registry = null, bool useHttps = false) : base(standardCollectors, registry)
2719
{
28-
_registry = registry ?? DefaultCollectorRegistry.Instance;
2920
var s = useHttps ? "s" : "";
3021
_httpListener.Prefixes.Add($"http{s}://{hostname}:{port}/{url}");
31-
if (_registry == DefaultCollectorRegistry.Instance)
32-
{
33-
// Default to DotNetStatsCollector if none speified
34-
// For no collectors, pass an empty collection
35-
if (standardCollectors == null)
36-
standardCollectors = new[] { new DotNetStatsCollector() };
37-
38-
DefaultCollectorRegistry.Instance.RegisterOnDemandCollectors(standardCollectors);
39-
}
4022
}
4123

42-
public void Start(IScheduler scheduler = null)
24+
protected override IDisposable StartLoop(IScheduler scheduler)
4325
{
4426
_httpListener.Start();
45-
46-
StartLoop(scheduler ?? Scheduler.Default);
47-
}
48-
49-
private void StartLoop(IScheduler scheduler)
50-
{
5127
//delegate allocations below - but that's fine as it's not really on the "critical path" (polled relatively infrequently) - and it's much more readable this way
52-
_schedulerDelegate = scheduler.Schedule(
28+
return scheduler.Schedule(
5329
repeatAction =>
5430
{
5531
try
@@ -92,9 +68,9 @@ private void StartLoop(IScheduler scheduler)
9268
);
9369
}
9470

95-
public void Stop()
71+
protected override void StopInner()
9672
{
97-
if (_schedulerDelegate != null) _schedulerDelegate.Dispose();
73+
base.StopInner();
9874
_httpListener.Stop();
9975
_httpListener.Close();
10076
}

prometheus-net/prometheus-net.csproj

+3
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,15 @@
6262
<Compile Include="Gauge.cs" />
6363
<Compile Include="Histogram.cs" />
6464
<Compile Include="Advanced\ICollectorRegistry.cs" />
65+
<Compile Include="IMetricServer.cs" />
6566
<Compile Include="Internal\AsciiFormatter.cs" />
6667
<Compile Include="Advanced\ICollector.cs" />
6768
<Compile Include="Internal\LabelValues.cs" />
6869
<Compile Include="Advanced\Collector.cs" />
6970
<Compile Include="Internal\ProtoFormatter.cs" />
7071
<Compile Include="Advanced\MetricFactory.cs" />
72+
<Compile Include="MetricHandler.cs" />
73+
<Compile Include="MetricPusher.cs" />
7174
<Compile Include="Metrics.cs" />
7275
<Compile Include="MetricServer.cs" />
7376
<Compile Include="Advanced\DefaultCollectorRegistry.cs" />

tester/MetricPusherTester.cs

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Net;
6+
using System.Reactive.Concurrency;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
using Prometheus;
10+
11+
namespace tester
12+
{
13+
class MetricPusherTester : Tester
14+
{
15+
private IDisposable _schedulerDelegate;
16+
private HttpListener _httpListener;
17+
18+
public override IMetricServer InitializeMetricHandler()
19+
{
20+
return new MetricPusher(endpoint: "http://localhost:9091/metrics", job: "some_job");
21+
}
22+
23+
public override void OnStart()
24+
{
25+
_httpListener = new HttpListener();
26+
_httpListener.Prefixes.Add("http://localhost:9091/");
27+
_httpListener.Start();
28+
_schedulerDelegate = Scheduler.Default.Schedule(
29+
action =>
30+
{
31+
try
32+
{
33+
if (!_httpListener.IsListening)
34+
{
35+
return;
36+
}
37+
var httpListenerContext = _httpListener.GetContext();
38+
var request = httpListenerContext.Request;
39+
var response = httpListenerContext.Response;
40+
41+
PrintRequestDetails(request.Url);
42+
43+
string body;
44+
using (var reader = new StreamReader(request.InputStream))
45+
{
46+
body = reader.ReadToEnd();
47+
}
48+
Console.WriteLine(body);
49+
response.StatusCode = 204;
50+
response.Close();
51+
action.Invoke();
52+
}
53+
catch (HttpListenerException)
54+
{
55+
// Ignore possible exception at the end of the test
56+
}
57+
});
58+
}
59+
60+
public override void OnEnd()
61+
{
62+
_httpListener.Stop();
63+
_httpListener.Close();
64+
_schedulerDelegate.Dispose();
65+
}
66+
67+
private void PrintRequestDetails(Uri requestUrl)
68+
{
69+
var segments = requestUrl.Segments;
70+
int idx;
71+
if (segments.Length < 2 || (idx = Array.IndexOf(segments, "job/")) < 0)
72+
{
73+
Console.WriteLine("# Unexpected label information");
74+
return;
75+
}
76+
StringBuilder sb = new StringBuilder("#");
77+
for (int i = idx; i < segments.Length; i++)
78+
{
79+
if (i == segments.Length - 1)
80+
{
81+
continue;
82+
}
83+
sb.AppendFormat(" {0}: {1} |", segments[i].TrimEnd('/'), segments[++i].TrimEnd('/'));
84+
}
85+
Console.WriteLine(sb.ToString().TrimEnd('|'));
86+
}
87+
}
88+
}

tester/MetricServerTester.cs

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Net;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
using Prometheus;
9+
10+
namespace tester
11+
{
12+
class MetricServerTester : Tester
13+
{
14+
public override IMetricServer InitializeMetricHandler()
15+
{
16+
return new MetricServer(hostname: "localhost", port: 1234);
17+
}
18+
19+
public override void OnObservation()
20+
{
21+
var httpRequest = (HttpWebRequest)WebRequest.Create("http://localhost:1234/metrics");
22+
httpRequest.Method = "GET";
23+
24+
using (var httpResponse = (HttpWebResponse)httpRequest.GetResponse())
25+
{
26+
var text = new StreamReader(httpResponse.GetResponseStream()).ReadToEnd();
27+
Console.WriteLine(text);
28+
}
29+
}
30+
}
31+
}

0 commit comments

Comments
 (0)