Skip to content

Commit c7a7ce0

Browse files
author
Dean Ward
committed
Add a sample custom handler based on BufferedHttpMetricHandler
1 parent a0a6d9c commit c7a7ce0

File tree

3 files changed

+165
-3
lines changed

3 files changed

+165
-3
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
using System;
2+
using System.Buffers;
3+
using System.Buffers.Text;
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Net.Http;
7+
using System.Net.Http.Headers;
8+
using System.Text;
9+
using System.Threading.Tasks;
10+
using StackExchange.Metrics.Infrastructure;
11+
12+
namespace StackExchange.Metrics.SampleHost
13+
{
14+
/// <summary>
15+
/// A simple handler that serializes metrics as plain text in the format:
16+
/// &lt;metric_name&gt;,&ltmetric_value&gt;\n
17+
/// </summary>
18+
public class MyCustomHandler : BufferedHttpMetricHandler
19+
{
20+
private static readonly MediaTypeHeaderValue _plainText = new MediaTypeHeaderValue("text/plain");
21+
22+
private Uri _uri;
23+
24+
/// <summary>
25+
/// Constructs a new handler pointing at the specified <see cref="System.Uri" />.
26+
/// </summary>
27+
/// <param name="uri">
28+
/// <see cref="System.Uri" /> of the HTTP endpoint.
29+
/// </param>
30+
public MyCustomHandler(Uri uri)
31+
{
32+
Uri = uri;
33+
}
34+
35+
/// <summary>
36+
/// Gets or sets the URI used by the handler.
37+
/// </summary>
38+
public Uri Uri
39+
{
40+
get; set;
41+
}
42+
43+
// no pre or post-amble for this handler
44+
protected override int GetPostambleLength(PayloadType payloadType) => 0;
45+
protected override int GetPreambleLength(PayloadType payloadType) => 0;
46+
47+
// usually used to trim things from the sequence; e.g. trailing commas or line breaks
48+
protected override void PrepareSequence(ref ReadOnlySequence<byte> sequence, PayloadType payloadType)
49+
{
50+
}
51+
52+
protected override HttpClient CreateHttpClient()
53+
{
54+
var httpClient = base.CreateHttpClient();
55+
// TODO: sane auth implementation
56+
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", "...");
57+
return httpClient;
58+
}
59+
60+
protected override ValueTask SendCounterAsync(ReadOnlySequence<byte> sequence) => SendAsync(_uri, HttpMethod.Post, PayloadType.Counter, _plainText, sequence);
61+
protected override ValueTask SendCumulativeCounterAsync(ReadOnlySequence<byte> sequence) => SendAsync(_uri, HttpMethod.Post, PayloadType.CumulativeCounter, _plainText, sequence);
62+
protected override ValueTask SendGaugeAsync(ReadOnlySequence<byte> sequence) => SendAsync(_uri, HttpMethod.Post, PayloadType.Gauge, _plainText, sequence);
63+
// this implementation doesn't support metadata
64+
protected override ValueTask SendMetadataAsync(ReadOnlySequence<byte> sequence) => default;
65+
66+
protected override void SerializeMetadata(IBufferWriter<byte> writer, IEnumerable<Metadata> metadata)
67+
{
68+
// this implementation doesn't support metadata
69+
}
70+
71+
private const int ValueDecimals = 5;
72+
private static readonly byte[] s_comma = Encoding.UTF8.GetBytes(",");
73+
private static readonly byte[] s_newLine = Encoding.UTF8.GetBytes("\n");
74+
static readonly StandardFormat s_valueFormat = StandardFormat.Parse("F" + ValueDecimals);
75+
76+
protected override void SerializeMetric(IBufferWriter<byte> writer, in MetricReading reading)
77+
{
78+
// first calculate how much space we need
79+
var encoding = Encoding.UTF8;
80+
var length = encoding.GetByteCount(reading.Name) + s_comma.Length;
81+
82+
// calculate the length needed to render the value
83+
var value = reading.Value;
84+
var valueIsWhole = value % 1 == 0;
85+
int valueLength = 1; // first digit
86+
if (!valueIsWhole)
87+
{
88+
valueLength += 1 + ValueDecimals; // + decimal point + decimal digits
89+
}
90+
91+
// calculate the remaining digits before the decimal point
92+
var valueAsLong = (long)value;
93+
while ((valueAsLong /= 10) > 0)
94+
{
95+
valueLength++;
96+
}
97+
98+
length += valueLength;
99+
length += s_newLine.Length;
100+
101+
// go grab some space in the buffer writer
102+
var memory = writer.GetMemory(length);
103+
// we also need a byte array otherwise we can't do all the encoding bits
104+
var arraySegment = memory.GetArray();
105+
var buffer = arraySegment.Array;
106+
var bytesWritten = arraySegment.Offset;
107+
// write data into the buffer
108+
{
109+
// write the name into the buffer
110+
bytesWritten += encoding.GetBytes(reading.Name, 0, reading.Name.Length, buffer, bytesWritten);
111+
112+
// separator (,)
113+
CopyToBuffer(s_comma);
114+
115+
// write the value as a long
116+
var valueBytesWritten = 0;
117+
if (valueIsWhole)
118+
{
119+
if (!Utf8Formatter.TryFormat((long)reading.Value, buffer.AsSpan(bytesWritten, valueLength), out valueBytesWritten))
120+
{
121+
var ex = new InvalidOperationException(
122+
"Span was not big enough to write metric value"
123+
);
124+
125+
ex.Data.Add("Value", valueAsLong.ToString());
126+
ex.Data.Add("Size", valueLength.ToString());
127+
throw ex;
128+
}
129+
}
130+
// write the value as a fixed point (f5) decimal
131+
else if (!Utf8Formatter.TryFormat(reading.Value, buffer.AsSpan(bytesWritten, valueLength), out valueBytesWritten, s_valueFormat))
132+
{
133+
var ex = new InvalidOperationException(
134+
"Span was not big enough to write metric value"
135+
);
136+
137+
ex.Data.Add("Value", value.ToString("f5"));
138+
ex.Data.Add("Size", valueLength.ToString());
139+
throw ex;
140+
}
141+
142+
bytesWritten += valueBytesWritten;
143+
144+
// new line (\n)
145+
CopyToBuffer(s_newLine);
146+
147+
// now write it to the buffer writer
148+
writer.Write(memory.Span.Slice(0, length));
149+
150+
void CopyToBuffer(byte[] source)
151+
{
152+
Array.Copy(source, 0, buffer, bytesWritten, source.Length);
153+
bytesWritten += source.Length;
154+
}
155+
}
156+
}
157+
158+
protected override Task WritePostambleAsync(Stream stream, PayloadType payloadType) => Task.CompletedTask;
159+
protected override Task WritePreambleAsync(Stream stream, PayloadType payloadType) => Task.CompletedTask;
160+
}
161+
}

samples/StackExchange.Metrics.SampleHost/Startup.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public void ConfigureServices(IServiceCollection services)
3434
)
3535
.AddDefaultSources()
3636
.AddSource<AppMetricSource>()
37+
.AddEndpoint("My Endpoint", new MyCustomHandler(new Uri("http://127.0.0.1:8080")))
3738
.UseExceptionHandler(ex => Console.WriteLine(ex))
3839
.Configure(
3940
o =>

src/StackExchange.Metrics/Infrastructure/MemoryExtensions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77
namespace StackExchange.Metrics.Infrastructure
88
{
9-
internal static class MemoryExtensions
9+
public static class MemoryExtensions
1010
{
1111
#pragma warning disable RCS1231 // Make parameter ref read-only.
12-
internal static ArraySegment<byte> GetArray(this Memory<byte> buffer) => GetArray((ReadOnlyMemory<byte>)buffer);
13-
internal static ArraySegment<byte> GetArray(this ReadOnlyMemory<byte> buffer)
12+
public static ArraySegment<byte> GetArray(this Memory<byte> buffer) => GetArray((ReadOnlyMemory<byte>)buffer);
13+
public static ArraySegment<byte> GetArray(this ReadOnlyMemory<byte> buffer)
1414
#pragma warning restore RCS1231 // Make parameter ref read-only.
1515
{
1616
if (!MemoryMarshal.TryGetArray(buffer, out var segment))

0 commit comments

Comments
 (0)