Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Metadata Support For Metrics #2939

Merged
merged 6 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions documentation/api/livemetrics-custom.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ The expected content type is `application/json`.
### Sample Request

```http
GET /livemetrics?pid=21632&durationSeconds=60 HTTP/1.1
POST /livemetrics?pid=21632&durationSeconds=60 HTTP/1.1
Host: localhost:52323
Authorization: Bearer fffffffffffffffffffffffffffffffffffffffffff=

Expand Down Expand Up @@ -91,7 +91,11 @@ Content-Type: application/json-seq
"displayName": "Counter 1",
"unit": "B",
"counterType": "Metric",
"value": 3
"value": 3,
"metadata": {
"MyKey 1": "MyValue 1",
"MyKey 2": "MyValue 2"
}
}
{
"timestamp": "2021-08-31T16:58:39.7515128+00:00",
Expand All @@ -100,7 +104,8 @@ Content-Type: application/json-seq
"displayName": "Counter 2",
"unit": "MB",
"counterType": "Metric",
"value": 126
"value": 126,
"metadata": {}
}
```

Expand Down
2 changes: 2 additions & 0 deletions documentation/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,8 @@ Additional metrics providers and counter names to return from this route can be

When `CounterNames` are not specified, all the counters associated with the `ProviderName` are collected.

[7.1+] Custom metrics support labels for metadata. Metadata cannot include commas (`,`); the inclusion of a comma in metadata will result in all metadata being removed from the custom metric.

### Disable default providers

In addition to enabling custom providers, `dotnet monitor` also allows you to disable collection of the default providers. You can do so via the following configuration:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ protected override void SerializeCounter(Stream stream, ICounterPayload counter)

//Some versions of .Net return invalid metric numbers. See https://github.com/dotnet/runtime/pull/46938
writer.WriteNumber("value", double.IsNaN(counter.Value) ? 0.0 : counter.Value);

writer.WriteStartObject("metadata");
foreach (var kvPair in counter.Metadata)
{
writer.WriteString(kvPair.Key, kvPair.Value);
}
writer.WriteEndObject();

writer.WriteEndObject();
}
stream.WriteByte((byte)'\n');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ public async Task SnapshotMetrics(Stream outputStream, CancellationToken token)
string metricName = PrometheusDataModel.GetPrometheusNormalizedName(metricInfo.Provider, metricInfo.Name, metricInfo.Unit);
string metricType = "gauge";

var keyValuePairs = from pair in metricInfo.Metadata select PrometheusDataModel.GetPrometheusNormalizedLabel(pair.Key, pair.Value);
string metricLabels = string.Join(", ", keyValuePairs);

//TODO Some clr metrics claim to be incrementing, but are really gauges.

await writer.WriteLineAsync(FormattableString.Invariant($"# HELP {metricName} {metricInfo.DisplayName}"));
Expand All @@ -103,7 +106,7 @@ public async Task SnapshotMetrics(Stream outputStream, CancellationToken token)
foreach (var metric in metricGroup.Value)
{
string metricValue = PrometheusDataModel.GetPrometheusNormalizedValue(metric.Unit, metric.Value);
await WriteMetricDetails(writer, metric, metricName, metricValue);
await WriteMetricDetails(writer, metric, metricName, metricValue, metricLabels);
}
}
}
Expand All @@ -112,9 +115,14 @@ private static async Task WriteMetricDetails(
StreamWriter writer,
ICounterPayload metric,
string metricName,
string metricValue)
string metricValue,
string metricLabels)
{
await writer.WriteAsync(metricName);
if (!string.IsNullOrWhiteSpace(metricLabels))
{
await writer.WriteAsync("{" + metricLabels + "}");
}
await writer.WriteLineAsync(FormattableString.Invariant($" {metricValue} {new DateTimeOffset(metric.Timestamp).ToUnixTimeMilliseconds()}"));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi
{
internal static class PrometheusDataModel
{
private const char SeperatorChar = '_';
private const char SeparatorChar = '_';
private const char EqualsChar = '=';
private const char QuotationChar = '"';
private const char SlashChar = '\\';
private const char NewlineChar = '\n';

private static readonly Dictionary<string, string> KnownUnits = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
Expand All @@ -38,17 +42,30 @@ public static string GetPrometheusNormalizedName(string metricProvider, string m
StringBuilder builder = new StringBuilder(metricProvider.Length + metric.Length + (hasUnit ? baseUnit.Length + 1 : 0) + 1);

NormalizeString(builder, metricProvider, isProvider: true);
builder.Append(SeperatorChar);
builder.Append(SeparatorChar);
NormalizeString(builder, metric, isProvider: false);
if (hasUnit)
{
builder.Append(SeperatorChar);
builder.Append(SeparatorChar);
NormalizeString(builder, baseUnit, isProvider: false);
}

return builder.ToString();
}

public static string GetPrometheusNormalizedLabel(string key, string value)
{
StringBuilder builder = new StringBuilder(key.Length + 2 * value.Length + 3); // Includes =,", and ", as well as extra padding for potential escape characters in the value
kkeirstead marked this conversation as resolved.
Show resolved Hide resolved

NormalizeString(builder, key, isProvider: false);
builder.Append(EqualsChar);
builder.Append(QuotationChar);
NormalizeLabelValue(builder, value);
builder.Append(QuotationChar);

return builder.ToString();
}

public static string GetPrometheusNormalizedValue(string unit, double value)
{
if (string.Equals(unit, "MB", StringComparison.OrdinalIgnoreCase))
Expand All @@ -58,6 +75,32 @@ public static string GetPrometheusNormalizedValue(string unit, double value)
return value.ToString(CultureInfo.InvariantCulture);
}

private static void NormalizeLabelValue(StringBuilder builder, string value)
{
for (int i = 0; i < value.Length; i++)
{
if (value[i] == SlashChar)
{
builder.Append(SlashChar);
builder.Append(SlashChar);
}
else if (value[i] == NewlineChar)
{
builder.Append(SlashChar);
builder.Append('n');
}
else if (value[i] == QuotationChar)
{
builder.Append(SlashChar);
builder.Append(QuotationChar);
}
else
{
builder.Append(value[i]);
}
}
}

private static void NormalizeString(StringBuilder builder, string entity, bool isProvider)
{
//TODO We don't have any labels in the current metrics implementation, but may need to add support for it
Expand All @@ -73,14 +116,14 @@ private static void NormalizeString(StringBuilder builder, string entity, bool i
}
else if (!isProvider)
{
builder.Append(SeperatorChar);
builder.Append(SeparatorChar);
}
}

//CONSIDER Completely invalid providers such as '!@#$' will become '_'. Should we have a more obvious value for this?
if (allInvalid && isProvider)
{
builder.Append(SeperatorChar);
builder.Append(SeparatorChar);
}
}
private static bool IsValidChar(char c, bool isFirst)
Expand All @@ -90,7 +133,7 @@ private static bool IsValidChar(char c, bool isFirst)
return false;
}

if (c == SeperatorChar)
if (c == SeparatorChar)
{
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,21 @@ public void TestGetPrometheusNormalizedValue(string metricUnit, double metricVal
var normalizedValue = PrometheusDataModel.GetPrometheusNormalizedValue(metricUnit, metricValue);
Assert.Equal(normalizedValue, expectedValue);
}

[Theory()]
[InlineData("key", "value", "key=\"value\"")]
[InlineData("key*1", "value*1", "key_1=\"value*1\"")]
[InlineData("key 1", "value 1", "key_1=\"value 1\"")]
[InlineData("&*()", "Test\nice", "____=\"Test\\nice\"")]
[InlineData("", "Test\\nice", "=\"Test\\\\nice\"")]
[InlineData("Test\\test", "Test\\test", "Test_test=\"Test\\\\test\"")]
[InlineData("UnicodeάήΰLetter", "Test\\\nice", "Unicode___Letter=\"Test\\\\\\nice\"")]
[InlineData("ά", "Test\"quotes\"", "_=\"Test\\\"quotes\\\"\"")]
[InlineData("_key", "Test\\\"quotes\\\"", "_key=\"Test\\\\\\\"quotes\\\\\\\"\"")]
public void TestGetPrometheusNormalizedMetadataValue(string key, string value, string expectedLabel)
{
var normalizedLabel = PrometheusDataModel.GetPrometheusNormalizedLabel(key, value);
Assert.Equal(expectedLabel, normalizedLabel);
}
}
}