// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using Aspire.Dashboard.Components;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Model.MetricValues;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Tests;
using Google.Protobuf;
using Google.Protobuf.Collections;
using OpenTelemetry.Proto.Common.V1;
using OpenTelemetry.Proto.Metrics.V1;
using Xunit;
using static Aspire.Tests.Shared.Telemetry.TelemetryTestHelpers;
namespace Aspire.Dashboard.Tests.TelemetryRepositoryTests;
public abstract class MetricsTests : TelemetryRepositoryTestBase
{
private static readonly DateTime s_testTime = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
[Fact]
public async Task AddMetrics()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>()
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(1)),
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(2)),
CreateSumMetric(metricName: "test2", startTime: s_testTime.AddMinutes(1)),
}
},
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter2"),
Metrics =
{
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(1)),
CreateHistogramMetric(metricName: "test2", startTime: s_testTime.AddMinutes(1))
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var resources = repositoryContext.Repository.GetResources();
Assert.Collection(resources,
resource =>
{
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
});
var resourceView = Assert.Single(resources[0].GetViews());
Assert.Empty(resourceView.Properties);
var instruments = repositoryContext.Repository.GetInstrumentSummaries(resources[0].ResourceKey);
Assert.Collection(instruments,
instrument =>
{
Assert.Equal("test", instrument.Name);
Assert.Equal("Test metric description", instrument.Description);
Assert.Equal("widget", instrument.Unit);
Assert.Equal("test-meter", instrument.Parent.Name);
},
instrument =>
{
Assert.Equal("test2", instrument.Name);
Assert.Equal("Test metric description", instrument.Description);
Assert.Equal("widget", instrument.Unit);
Assert.Equal("test-meter", instrument.Parent.Name);
},
instrument =>
{
Assert.Equal("test", instrument.Name);
Assert.Equal("Test metric description", instrument.Description);
Assert.Equal("widget", instrument.Unit);
Assert.Equal("test-meter2", instrument.Parent.Name);
},
instrument =>
{
Assert.Equal("test2", instrument.Name);
Assert.Equal("Test metric description", instrument.Description);
Assert.Equal("widget", instrument.Unit);
Assert.Equal("test-meter2", instrument.Parent.Name);
});
var instrumentSummary = repositoryContext.Repository.GetInstrumentSummary(resources[0].ResourceKey, "test-meter2", "test2");
Assert.NotNull(instrumentSummary);
Assert.Equal(OtlpInstrumentType.Histogram, instrumentSummary.Type);
Assert.Null(repositoryContext.Repository.GetInstrumentSummary(resources[0].ResourceKey, "test-meter2", "missing"));
}
[Fact]
public async Task AddMetrics_MeterAttributeLimits_LimitsApplied()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync(maxAttributeCount: 5, maxAttributeLength: 16);
var metricAttributes = new List<KeyValuePair<string, string>>();
var meterAttributes = new List<KeyValuePair<string, string>>();
for (var i = 0; i < 10; i++)
{
var value = GetValue((i + 1) * 5);
metricAttributes.Add(new KeyValuePair<string, string>($"Metric_Key{i}", value));
meterAttributes.Add(new KeyValuePair<string, string>($"Meter_Key{i}", value));
}
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>()
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter", attributes: meterAttributes),
Metrics =
{
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(1), attributes: metricAttributes)
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var resources = repositoryContext.Repository.GetResources();
Assert.Collection(resources,
resource =>
{
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
});
var instrument = (await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resources[0].ResourceKey,
InstrumentName = "test",
MeterName = "test-meter",
StartTime = DateTime.MinValue,
EndTime = DateTime.MaxValue
}, cancellationToken: CancellationToken.None))!;
Assert.Collection(instrument.Summary.Parent.Attributes,
p =>
{
Assert.Equal("Meter_Key0", p.Key);
Assert.Equal("01234", p.Value);
},
p =>
{
Assert.Equal("Meter_Key1", p.Key);
Assert.Equal("0123456789", p.Value);
},
p =>
{
Assert.Equal("Meter_Key2", p.Key);
Assert.Equal("012345678901234", p.Value);
},
p =>
{
Assert.Equal("Meter_Key3", p.Key);
Assert.Equal("0123456789012345", p.Value);
},
p =>
{
Assert.Equal("Meter_Key4", p.Key);
Assert.Equal("0123456789012345", p.Value);
});
var dimensionAttributes = instrument.Dimensions.Single().Attributes;
Assert.Collection(dimensionAttributes,
p => Assert.Equal(KeyValuePair.Create("Metric_Key0", "01234"), p),
p => Assert.Equal(KeyValuePair.Create("Metric_Key1", "0123456789"), p),
p => Assert.Equal(KeyValuePair.Create("Metric_Key2", "012345678901234"), p),
p => Assert.Equal(KeyValuePair.Create("Metric_Key3", "0123456789012345"), p),
p => Assert.Equal(KeyValuePair.Create("Metric_Key4", "0123456789012345"), p),
p => Assert.Equal(KeyValuePair.Create("Meter_Key0", "01234"), p),
p => Assert.Equal(KeyValuePair.Create("Meter_Key1", "0123456789"), p),
p => Assert.Equal(KeyValuePair.Create("Meter_Key2", "012345678901234"), p),
p => Assert.Equal(KeyValuePair.Create("Meter_Key3", "0123456789012345"), p),
p => Assert.Equal(KeyValuePair.Create("Meter_Key4", "0123456789012345"), p));
Assert.Equal(10, instrument.KnownAttributeValues.Count);
}
[Fact]
public async Task AddMetrics_MetricAttributeLimits_LimitsApplied()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync(maxAttributeCount: 5, maxAttributeLength: 16);
var metricAttributes = new List<KeyValuePair<string, string>>();
var meterAttributes = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("Meter_Key0", GetValue(5))
};
for (var i = 0; i < 10; i++)
{
var value = GetValue((i + 1) * 5);
metricAttributes.Add(new KeyValuePair<string, string>($"Metric_Key{i}", value));
}
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>()
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter", attributes: meterAttributes),
Metrics =
{
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(1), attributes: metricAttributes)
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var resources = repositoryContext.Repository.GetResources();
Assert.Collection(resources,
resource =>
{
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
});
var instrument = (await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resources[0].ResourceKey,
InstrumentName = "test",
MeterName = "test-meter",
StartTime = DateTime.MinValue,
EndTime = DateTime.MaxValue
}, cancellationToken: CancellationToken.None))!;
Assert.Collection(instrument.Summary.Parent.Attributes,
p =>
{
Assert.Equal("Meter_Key0", p.Key);
Assert.Equal("01234", p.Value);
});
var dimensionAttributes = instrument.Dimensions.Single().Attributes;
Assert.Collection(dimensionAttributes,
p => Assert.Equal(KeyValuePair.Create("Metric_Key0", "01234"), p),
p => Assert.Equal(KeyValuePair.Create("Metric_Key1", "0123456789"), p),
p => Assert.Equal(KeyValuePair.Create("Metric_Key2", "012345678901234"), p),
p => Assert.Equal(KeyValuePair.Create("Metric_Key3", "0123456789012345"), p),
p => Assert.Equal(KeyValuePair.Create("Metric_Key4", "0123456789012345"), p),
p => Assert.Equal(KeyValuePair.Create("Meter_Key0", "01234"), p));
Assert.Equal(6, instrument.KnownAttributeValues.Count);
}
[Fact]
public async Task Metrics_ScopeAttributesAreMergedIntoDimensionsOnRead()
{
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope("TestMeter", attributes: [KeyValuePair.Create("scope-key", "scope-value")]),
Metrics =
{
CreateSumMetric("requests", s_testTime, attributes: [KeyValuePair.Create("point-key", "point-value")])
}
}
}
}
});
var resource = Assert.Single(repositoryContext.Repository.GetResources());
var instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resource.ResourceKey,
MeterName = "TestMeter",
InstrumentName = "requests",
StartTime = DateTime.MinValue,
EndTime = DateTime.MaxValue
}, cancellationToken: CancellationToken.None);
Assert.NotNull(instrument);
Assert.Equal(
[KeyValuePair.Create("point-key", "point-value"), KeyValuePair.Create("scope-key", "scope-value")],
Assert.Single(instrument.Dimensions).Attributes);
}
[Fact]
public void RoundtripSeconds()
{
var start = s_testTime.AddMinutes(1);
var nanoSeconds = DateTimeToUnixNanoseconds(start);
var end = OtlpHelpers.UnixNanoSecondsToDateTime(nanoSeconds);
Assert.Equal(start, end);
}
[Fact]
public async Task GetInstrument()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>()
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(1), exemplars: new List<Exemplar> { CreateExemplar(startTime: s_testTime.AddMinutes(1), value: 2, attributes: [KeyValuePair.Create("key1", "value1")]) }),
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(2)),
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(1), attributes: [KeyValuePair.Create("key1", "value1")]),
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(1), attributes: [KeyValuePair.Create("key1", "value2")]),
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(1), attributes: [KeyValuePair.Create("key1", "value1"), KeyValuePair.Create("key2", "value1")]),
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(1), attributes: [KeyValuePair.Create("key1", "value1"), KeyValuePair.Create("key2", "")])
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var resources = repositoryContext.Repository.GetResources();
Assert.Collection(resources,
resource =>
{
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
});
var instrumentData = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resources[0].ResourceKey,
InstrumentName = "test",
MeterName = "test-meter",
StartTime = s_testTime.AddMinutes(1),
EndTime = s_testTime.AddMinutes(1.5),
}, cancellationToken: CancellationToken.None);
Assert.NotNull(instrumentData);
Assert.Equal("test", instrumentData.Summary.Name);
Assert.Equal("Test metric description", instrumentData.Summary.Description);
Assert.Equal("widget", instrumentData.Summary.Unit);
Assert.Equal("test-meter", instrumentData.Summary.Parent.Name);
Assert.Collection(instrumentData.KnownAttributeValues.OrderBy(kvp => kvp.Key),
e =>
{
Assert.Equal("key1", e.Key);
Assert.Equal(new[] { null, "value1", "value2" }, e.Value);
},
e =>
{
Assert.Equal("key2", e.Key);
Assert.Equal(new[] { null, "value1", "" }, e.Value);
});
Assert.Equal(5, instrumentData.Dimensions.Count);
Assert.All(instrumentData.Dimensions, dimension => Assert.Equal(1, Assert.IsType<MetricValue<long>>(Assert.Single(dimension.Values)).Value));
var dimensionWithoutAttributes = Assert.Single(instrumentData.Dimensions, dimension => dimension.Attributes.Length == 0);
var exemplar = Assert.Single(dimensionWithoutAttributes.Values.SelectMany(value => value.Exemplars));
Assert.Equal("key1", exemplar.Attributes[0].Key);
Assert.Equal("value1", exemplar.Attributes[0].Value);
var filteredInstrumentData = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resources[0].ResourceKey,
InstrumentName = "test",
MeterName = "test-meter",
StartTime = s_testTime.AddMinutes(1),
EndTime = s_testTime.AddMinutes(1.5),
DimensionFilters = new Dictionary<string, IReadOnlyList<string?>>
{
["key1"] = ["value1"]
}
}, cancellationToken: CancellationToken.None);
Assert.NotNull(filteredInstrumentData);
Assert.Equal(3, filteredInstrumentData.Dimensions.Count);
Assert.All(filteredInstrumentData.Dimensions, dimension =>
{
Assert.Contains(KeyValuePair.Create("key1", "value1"), dimension.Attributes);
Assert.Equal(1, Assert.IsType<MetricValue<long>>(Assert.Single(dimension.Values)).Value);
});
Assert.Equal(instrumentData.KnownAttributeValues, filteredInstrumentData.KnownAttributeValues);
}
[Fact]
public async Task GetInstrument_StaggeredDimensionChanges_ReturnsCurrentValues()
{
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(1), value: 10, attributes: [KeyValuePair.Create("dimension", "stable")]),
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(1), value: 20, attributes: [KeyValuePair.Create("dimension", "changing")])
}
}
}
},
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(2), value: 10, attributes: [KeyValuePair.Create("dimension", "stable")]),
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(2), value: 25, attributes: [KeyValuePair.Create("dimension", "changing")])
}
}
}
}
});
Assert.Equal(0, addContext.FailureCount);
var resource = Assert.Single(repositoryContext.Repository.GetResources());
var instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resource.ResourceKey,
MeterName = "test-meter",
InstrumentName = "test",
StartTime = s_testTime,
EndTime = s_testTime.AddMinutes(3)
}, cancellationToken: CancellationToken.None);
Assert.NotNull(instrument);
var dimensions = instrument.Dimensions.ToDictionary(dimension => Assert.Single(dimension.Attributes).Value);
Assert.Equal(10, Assert.IsType<MetricValue<long>>(Assert.Single(dimensions["stable"].Values)).Value);
Assert.Equal(25, Assert.IsType<MetricValue<long>>(dimensions["changing"].Values[^1]).Value);
}
[Fact]
public async Task GetInstrumentLatestEndTime()
{
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(1)),
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(2), attributes: [KeyValuePair.Create("key", "value")])
}
}
}
}
});
var resourceKey = Assert.Single(repositoryContext.Repository.GetResources()).ResourceKey;
Assert.Equal(s_testTime.AddMinutes(2), repositoryContext.Repository.GetInstrumentLatestEndTime(resourceKey, "test-meter", "test"));
Assert.Null(repositoryContext.Repository.GetInstrumentLatestEndTime(resourceKey, "test-meter", "missing"));
}
protected static Exemplar CreateExemplar(DateTime startTime, double value, IEnumerable<KeyValuePair<string, string>>? attributes = null)
{
var exemplar = new Exemplar
{
TimeUnixNano = DateTimeToUnixNanoseconds(startTime),
AsDouble = value,
SpanId = ByteString.CopyFrom(Encoding.UTF8.GetBytes("span-id")),
TraceId = ByteString.CopyFrom(Encoding.UTF8.GetBytes("trace-id"))
};
if (attributes != null)
{
foreach (var attribute in attributes)
{
exemplar.FilteredAttributes.Add(new KeyValue { Key = attribute.Key, Value = new AnyValue { StringValue = attribute.Value } });
}
}
return exemplar;
}
[Fact]
public async Task AddMetrics_Capacity_ValuesRemoved()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync(maxMetricsCount: 3);
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>()
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(1), value: 1),
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(2), value: 2),
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(3), value: 3),
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(4), value: 4),
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(5), value: 5),
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var resources = repositoryContext.Repository.GetResources();
Assert.Collection(resources,
resource =>
{
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
});
var instrument = (await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resources[0].ResourceKey,
InstrumentName = "test",
MeterName = "test-meter",
StartTime = DateTime.MinValue,
EndTime = DateTime.MaxValue
}, cancellationToken: CancellationToken.None))!;
Assert.Equal("test", instrument.Summary.Name);
Assert.Equal("Test metric description", instrument.Summary.Description);
Assert.Equal("widget", instrument.Summary.Unit);
Assert.Equal("test-meter", instrument.Summary.Parent.Name);
// Only the last 3 values should be kept.
var dimension = Assert.Single(instrument.Dimensions);
Assert.Collection(dimension.Values,
m =>
{
Assert.Equal(s_testTime.AddMinutes(2), m.Start);
Assert.Equal(s_testTime.AddMinutes(3), m.End);
Assert.Equal(3, ((MetricValue<long>)m).Value);
},
m =>
{
Assert.Equal(s_testTime.AddMinutes(3), m.Start);
Assert.Equal(s_testTime.AddMinutes(4), m.End);
Assert.Equal(4, ((MetricValue<long>)m).Value);
},
m =>
{
Assert.Equal(s_testTime.AddMinutes(4), m.Start);
Assert.Equal(s_testTime.AddMinutes(5), m.End);
Assert.Equal(5, ((MetricValue<long>)m).Value);
});
}
[Fact]
public async Task GetMetrics_MultipleInstances()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>()
{
new ResourceMetrics
{
Resource = CreateResource(name: "resource1", instanceId: "123"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test1", value: 1, startTime: s_testTime.AddMinutes(1), attributes: [KeyValuePair.Create("key-1", "value-1")]),
CreateSumMetric(metricName: "test1", value: 2, startTime: s_testTime.AddMinutes(1), attributes: [KeyValuePair.Create("key-1", "value-2")])
}
}
}
},
new ResourceMetrics
{
Resource = CreateResource(name: "resource1", instanceId: "456"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test1", value: 3, startTime: s_testTime.AddMinutes(1), attributes: [KeyValuePair.Create("key-1", "value-3")]),
CreateSumMetric(metricName: "test2", value: 4, startTime: s_testTime.AddMinutes(1), attributes: [KeyValuePair.Create("key-1", "value-4")])
}
}
}
},
new ResourceMetrics
{
Resource = CreateResource(name: "resource2"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test1", value: 5, startTime: s_testTime.AddMinutes(1), attributes: [KeyValuePair.Create("key-1", "value-5")]),
CreateSumMetric(metricName: "test3", value: 6, startTime: s_testTime.AddMinutes(1), attributes: [KeyValuePair.Create("key-1", "value-6")])
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var resourceKey = new ResourceKey("resource1", InstanceId: null);
var instruments = repositoryContext.Repository.GetInstrumentSummaries(resourceKey);
Assert.Collection(instruments,
instrument =>
{
Assert.Equal("test1", instrument.Name);
Assert.Equal("Test metric description", instrument.Description);
Assert.Equal("widget", instrument.Unit);
Assert.Equal("test-meter", instrument.Parent.Name);
},
instrument =>
{
Assert.Equal("test2", instrument.Name);
Assert.Equal("Test metric description", instrument.Description);
Assert.Equal("widget", instrument.Unit);
Assert.Equal("test-meter", instrument.Parent.Name);
});
var instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resourceKey,
InstrumentName = "test1",
MeterName = "test-meter",
StartTime = s_testTime,
EndTime = s_testTime.AddMinutes(20)
}, cancellationToken: CancellationToken.None);
Assert.NotNull(instrument);
Assert.Equal("test1", instrument.Summary.Name);
Assert.Collection(
instrument.Dimensions.OrderBy(dimension => Assert.Single(dimension.Attributes).Value),
dimension => Assert.Equal(1, Assert.IsType<MetricValue<long>>(Assert.Single(dimension.Values)).Value),
dimension => Assert.Equal(2, Assert.IsType<MetricValue<long>>(Assert.Single(dimension.Values)).Value),
dimension => Assert.Equal(3, Assert.IsType<MetricValue<long>>(Assert.Single(dimension.Values)).Value));
var knownValues = Assert.Single(instrument.KnownAttributeValues);
Assert.Equal("key-1", knownValues.Key);
Assert.Collection(knownValues.Value.Order(),
v => Assert.Equal("value-1", v),
v => Assert.Equal("value-2", v),
v => Assert.Equal("value-3", v));
}
[Fact]
public async Task RemoveMetrics_All()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>()
{
new ResourceMetrics
{
Resource = CreateResource(name: "resource1", instanceId: "123"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test1", value: 1, startTime: s_testTime.AddMinutes(1)),
CreateSumMetric(metricName: "test1", value: 2, startTime: s_testTime.AddMinutes(1))
}
}
}
},
new ResourceMetrics
{
Resource = CreateResource(name: "resource1", instanceId: "456"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test1", value: 3, startTime: s_testTime.AddMinutes(1)),
CreateSumMetric(metricName: "test2", value: 4, startTime: s_testTime.AddMinutes(1))
}
}
}
},
new ResourceMetrics
{
Resource = CreateResource(name: "resource2"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test1", value: 5, startTime: s_testTime.AddMinutes(1)),
CreateSumMetric(metricName: "test3", value: 6, startTime: s_testTime.AddMinutes(1))
}
}
}
}
});
// Act
await repositoryContext.Repository.AsWriter().ClearMetricsAsync();
// Assert
Assert.Equal(0, addContext.FailureCount);
var resource1Key = new ResourceKey("resource1", InstanceId: null);
var resource1Instruments = repositoryContext.Repository.GetInstrumentSummaries(resource1Key);
Assert.Empty(resource1Instruments);
var resource2Key = new ResourceKey("resource2", InstanceId: null);
var resource2Instruments = repositoryContext.Repository.GetInstrumentSummaries(resource2Key);
Assert.Empty(resource2Instruments);
}
[Fact]
public async Task RemoveMetrics_SelectedResource()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>()
{
new ResourceMetrics
{
Resource = CreateResource(name: "resource1", instanceId: "123"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test1", value: 1, startTime: s_testTime.AddMinutes(1)),
CreateSumMetric(metricName: "test1", value: 2, startTime: s_testTime.AddMinutes(1))
}
}
}
},
new ResourceMetrics
{
Resource = CreateResource(name: "resource1", instanceId: "456"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test1", value: 3, startTime: s_testTime.AddMinutes(1)),
CreateSumMetric(metricName: "test2", value: 4, startTime: s_testTime.AddMinutes(1))
}
}
}
},
new ResourceMetrics
{
Resource = CreateResource(name: "resource2"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test1", value: 5, startTime: s_testTime.AddMinutes(1)),
CreateSumMetric(metricName: "test3", value: 6, startTime: s_testTime.AddMinutes(1))
}
}
}
}
});
// Act
await repositoryContext.Repository.AsWriter().ClearMetricsAsync(new ResourceKey("resource1", "456"));
// Assert
Assert.Equal(0, addContext.FailureCount);
var resource1Key = new ResourceKey("resource1", InstanceId: null);
var resource1Instruments = repositoryContext.Repository.GetInstrumentSummaries(resource1Key);
var resource1Instrument = Assert.Single(resource1Instruments);
Assert.Equal("test1", resource1Instrument.Name);
Assert.Equal("Test metric description", resource1Instrument.Description);
Assert.Equal("widget", resource1Instrument.Unit);
Assert.Equal("test-meter", resource1Instrument.Parent.Name);
var resource1Test1Instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resource1Key,
InstrumentName = "test1",
MeterName = "test-meter",
StartTime = s_testTime,
EndTime = s_testTime.AddMinutes(20)
}, cancellationToken: CancellationToken.None);
Assert.NotNull(resource1Test1Instrument);
Assert.Equal("test1", resource1Test1Instrument.Summary.Name);
var resource1Test1Dimensions = Assert.Single(resource1Test1Instrument.Dimensions);
Assert.Equal(2, Assert.IsType<MetricValue<long>>(resource1Test1Dimensions.Values[^1]).Value);
var resource1Test2Instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resource1Key,
InstrumentName = "test2",
MeterName = "test-meter",
StartTime = s_testTime,
EndTime = s_testTime.AddMinutes(20)
}, cancellationToken: CancellationToken.None);
Assert.Null(resource1Test2Instrument);
var resource2Key = new ResourceKey("resource2", InstanceId: null);
var resource2Instruments = repositoryContext.Repository.GetInstrumentSummaries(resource2Key);
Assert.Collection(resource2Instruments,
instrument =>
{
Assert.Equal("test1", instrument.Name);
Assert.Equal("Test metric description", instrument.Description);
Assert.Equal("widget", instrument.Unit);
Assert.Equal("test-meter", instrument.Parent.Name);
},
instrument =>
{
Assert.Equal("test3", instrument.Name);
Assert.Equal("Test metric description", instrument.Description);
Assert.Equal("widget", instrument.Unit);
Assert.Equal("test-meter", instrument.Parent.Name);
});
var resource2Test1Instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resource2Key,
InstrumentName = "test1",
MeterName = "test-meter",
StartTime = s_testTime,
EndTime = s_testTime.AddMinutes(20)
}, cancellationToken: CancellationToken.None);
Assert.NotNull(resource2Test1Instrument);
Assert.Equal("test1", resource2Test1Instrument.Summary.Name);
var resource2Test1Dimensions = Assert.Single(resource2Test1Instrument.Dimensions);
Assert.Equal(5, ((MetricValue<long>)resource2Test1Dimensions.Values.Single()).Value);
var resource2Test3Instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resource2Key,
InstrumentName = "test3",
MeterName = "test-meter",
StartTime = s_testTime,
EndTime = s_testTime.AddMinutes(20)
}, cancellationToken: CancellationToken.None);
Assert.NotNull(resource2Test3Instrument);
Assert.Equal("test3", resource2Test3Instrument.Summary.Name);
var resource2Test3Dimensions = Assert.Single(resource2Test3Instrument.Dimensions);
Assert.Equal(6, ((MetricValue<long>)resource2Test3Dimensions.Values.Single()).Value);
}
[Fact]
public async Task RemoveMetrics_MultipleSelectedResources()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>()
{
new ResourceMetrics
{
Resource = CreateResource(name: "resource1", instanceId: "123"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test1", value: 1, startTime: s_testTime.AddMinutes(1), attributes: [KeyValuePair.Create("key-1", "value-1")]),
CreateSumMetric(metricName: "test1", value: 2, startTime: s_testTime.AddMinutes(1), attributes: [KeyValuePair.Create("key-1", "value-2")]),
}
}
}
},
new ResourceMetrics
{
Resource = CreateResource(name: "resource1", instanceId: "456"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test1", value: 3, startTime: s_testTime.AddMinutes(1)),
CreateSumMetric(metricName: "test2", value: 4, startTime: s_testTime.AddMinutes(1))
}
}
}
},
new ResourceMetrics
{
Resource = CreateResource(name: "resource2"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test1", value: 5, startTime: s_testTime.AddMinutes(1)),
CreateSumMetric(metricName: "test3", value: 6, startTime: s_testTime.AddMinutes(1))
}
}
}
}
});
// Act
await repositoryContext.Repository.AsWriter().ClearMetricsAsync(new ResourceKey("resource1", null));
// Assert
Assert.Equal(0, addContext.FailureCount);
var resource1Key = new ResourceKey("resource1", InstanceId: null);
var resource1Instruments = repositoryContext.Repository.GetInstrumentSummaries(resource1Key);
Assert.Empty(resource1Instruments);
var resource1Test1Instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resource1Key,
InstrumentName = "test1",
MeterName = "test-meter",
StartTime = s_testTime,
EndTime = s_testTime.AddMinutes(20)
}, cancellationToken: CancellationToken.None);
Assert.Null(resource1Test1Instrument);
var resource1Test2Instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resource1Key,
InstrumentName = "test2",
MeterName = "test-meter",
StartTime = s_testTime,
EndTime = s_testTime.AddMinutes(20)
}, cancellationToken: CancellationToken.None);
Assert.Null(resource1Test2Instrument);
var resource2Key = new ResourceKey("resource2", InstanceId: null);
var resource2Instruments = repositoryContext.Repository.GetInstrumentSummaries(resource2Key);
Assert.Collection(resource2Instruments,
instrument =>
{
Assert.Equal("test1", instrument.Name);
Assert.Equal("Test metric description", instrument.Description);
Assert.Equal("widget", instrument.Unit);
Assert.Equal("test-meter", instrument.Parent.Name);
},
instrument =>
{
Assert.Equal("test3", instrument.Name);
Assert.Equal("Test metric description", instrument.Description);
Assert.Equal("widget", instrument.Unit);
Assert.Equal("test-meter", instrument.Parent.Name);
});
var resource2Test1Instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resource2Key,
InstrumentName = "test1",
MeterName = "test-meter",
StartTime = s_testTime,
EndTime = s_testTime.AddMinutes(20)
}, cancellationToken: CancellationToken.None);
Assert.NotNull(resource2Test1Instrument);
Assert.Equal("test1", resource2Test1Instrument.Summary.Name);
var resource2Test1Dimensions = Assert.Single(resource2Test1Instrument.Dimensions);
Assert.Equal(5, ((MetricValue<long>)resource2Test1Dimensions.Values.Single()).Value);
var resource2Test3Instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resource2Key,
InstrumentName = "test3",
MeterName = "test-meter",
StartTime = s_testTime,
EndTime = s_testTime.AddMinutes(20)
}, cancellationToken: CancellationToken.None);
Assert.NotNull(resource2Test3Instrument);
Assert.Equal("test3", resource2Test3Instrument.Summary.Name);
var resource2Test3Dimensions = Assert.Single(resource2Test3Instrument.Dimensions);
Assert.Equal(6, ((MetricValue<long>)resource2Test3Dimensions.Values.Single()).Value);
}
[Fact]
public async Task AddMetrics_InvalidInstrument()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
// Act
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>()
{
new ResourceMetrics
{
Resource = CreateResource(name: "resource1", instanceId: "123"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "", value: 1, startTime: s_testTime.AddMinutes(1), attributes: [KeyValuePair.Create("key-1", "value-1")]),
CreateSumMetric(metricName: "test1", value: 2, startTime: s_testTime.AddMinutes(1), attributes: [KeyValuePair.Create("key-1", "value-2")]),
}
}
}
}
});
// Assert
Assert.Equal(1, addContext.FailureCount);
var resource1Key = new ResourceKey("resource1", InstanceId: null);
var resource1Instruments = repositoryContext.Repository.GetInstrumentSummaries(resource1Key);
Assert.Collection(resource1Instruments,
instrument =>
{
Assert.Equal("test1", instrument.Name);
Assert.Equal("Test metric description", instrument.Description);
Assert.Equal("widget", instrument.Unit);
Assert.Equal("test-meter", instrument.Parent.Name);
});
}
[Fact]
public async Task AddMetrics_NonFiniteDoubleDataPointsRejectedIndividually()
{
using var repositoryContext = await CreateRepositoryAsync();
var metric = new Metric
{
Name = "test",
Sum = new Sum
{
AggregationTemporality = AggregationTemporality.Cumulative,
IsMonotonic = true
}
};
metric.Sum.DataPoints.AddRange(
[
CreateDoublePoint(1, 1, "first"),
CreateDoublePoint(double.NaN, 2),
CreateDoublePoint(double.PositiveInfinity, 3),
CreateDoublePoint(double.NegativeInfinity, 4),
CreateDoublePoint(2, 5, "second")
]);
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics = { metric }
}
}
}
});
Assert.Equal(2, addContext.SuccessCount);
Assert.Equal(3, addContext.FailureCount);
var instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = new ResourceKey("TestService", "TestId"),
MeterName = "test-meter",
InstrumentName = "test",
StartTime = DateTime.MinValue,
EndTime = DateTime.MaxValue
}, cancellationToken: CancellationToken.None);
Assert.Equal(
[1d, 2d],
instrument!.Dimensions
.OrderBy(dimension => Assert.Single(dimension.Attributes).Value)
.Select(dimension => Assert.IsType<MetricValue<double>>(Assert.Single(dimension.Values)).Value));
static NumberDataPoint CreateDoublePoint(double value, int minute, string? dimension = null)
{
var timestamp = DateTimeToUnixNanoseconds(s_testTime.AddMinutes(minute));
var point = new NumberDataPoint
{
AsDouble = value,
StartTimeUnixNano = timestamp,
TimeUnixNano = timestamp
};
if (dimension is not null)
{
point.Attributes.Add(new KeyValue
{
Key = "dimension",
Value = new AnyValue { StringValue = dimension }
});
}
return point;
}
}
[Theory]
[InlineData(double.NaN)]
[InlineData(double.PositiveInfinity)]
[InlineData(double.NegativeInfinity)]
public async Task AddMetrics_NonFiniteHistogramSumRejected(double sum)
{
using var repositoryContext = await CreateRepositoryAsync();
var metric = CreateHistogramMetric(metricName: "test", startTime: s_testTime.AddMinutes(1));
metric.Histogram.DataPoints[0].Sum = sum;
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics = { metric }
}
}
}
});
Assert.Equal(0, addContext.SuccessCount);
Assert.Equal(1, addContext.FailureCount);
var instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = new ResourceKey("TestService", "TestId"),
MeterName = "test-meter",
InstrumentName = "test",
StartTime = DateTime.MinValue,
EndTime = DateTime.MaxValue
}, cancellationToken: CancellationToken.None);
Assert.All(instrument!.Dimensions, dimension => Assert.Empty(dimension.Values));
}
[Fact]
public async Task AddMetrics_NonFiniteExemplarsIgnoredWithoutRejectingPoint()
{
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(
metricName: "test",
startTime: s_testTime.AddMinutes(1),
exemplars:
[
CreateExemplar(s_testTime.AddMinutes(1), double.NaN),
CreateExemplar(s_testTime.AddMinutes(2), double.PositiveInfinity),
CreateExemplar(s_testTime.AddMinutes(3), double.NegativeInfinity),
CreateExemplar(s_testTime.AddMinutes(4), 2)
])
}
}
}
}
});
Assert.Equal(1, addContext.SuccessCount);
Assert.Equal(0, addContext.FailureCount);
var instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = new ResourceKey("TestService", "TestId"),
MeterName = "test-meter",
InstrumentName = "test",
StartTime = DateTime.MinValue,
EndTime = DateTime.MaxValue
}, cancellationToken: CancellationToken.None);
var value = Assert.Single(Assert.Single(instrument!.Dimensions).Values);
Assert.Equal(2, Assert.Single(value.Exemplars).Value);
}
[Fact]
public async Task AddMetrics_InvalidHistogramDataPoints()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
var histogramMetric = new Metric
{
Name = "test",
Description = "Test metric description",
Unit = "widget",
Histogram = new Histogram
{
AggregationTemporality = AggregationTemporality.Cumulative,
DataPoints =
{
new HistogramDataPoint
{
Count = 6,
Sum = 1,
ExplicitBounds = { },
BucketCounts = { 1 },
TimeUnixNano = DateTimeToUnixNanoseconds(s_testTime.AddMinutes(1))
},
new HistogramDataPoint
{
Count = 6,
Sum = 1,
ExplicitBounds = { },
BucketCounts = { 1 },
TimeUnixNano = DateTimeToUnixNanoseconds(s_testTime.AddMinutes(2))
},
new HistogramDataPoint
{
Count = 6,
Sum = 1,
ExplicitBounds = { 1, 2, 3 },
BucketCounts = { 1, 2, 3 },
TimeUnixNano = DateTimeToUnixNanoseconds(s_testTime.AddMinutes(3))
}
}
}
};
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>()
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics = { histogramMetric }
}
}
}
});
// Assert
Assert.Equal(2, addContext.FailureCount);
var resources = Assert.Single(repositoryContext.Repository.GetResources());
var instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resources.ResourceKey,
MeterName = "test-meter",
InstrumentName = "test",
StartTime = DateTime.MinValue,
EndTime = DateTime.MaxValue
}, cancellationToken: CancellationToken.None);
Assert.NotNull(instrument);
Assert.Equal("test", instrument.Summary.Name);
Assert.Equal("Test metric description", instrument.Summary.Description);
Assert.Equal("widget", instrument.Summary.Unit);
Assert.Equal("test-meter", instrument.Summary.Parent.Name);
var dimension = Assert.Single(instrument.Dimensions);
Assert.Single(dimension.Values);
}
[Fact]
public async Task AddMetrics_HistogramBucketCountLengthChanges_DataPointRejected()
{
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
var histogramMetric = new Metric
{
Name = "test",
Histogram = new Histogram
{
AggregationTemporality = AggregationTemporality.Cumulative,
DataPoints =
{
new HistogramDataPoint
{
Count = 6,
ExplicitBounds = { 1, 2 },
BucketCounts = { 1, 2, 3 },
TimeUnixNano = DateTimeToUnixNanoseconds(s_testTime.AddMinutes(1))
},
new HistogramDataPoint
{
Count = 10,
ExplicitBounds = { 1, 2, 3 },
BucketCounts = { 1, 2, 3, 4 },
TimeUnixNano = DateTimeToUnixNanoseconds(s_testTime.AddMinutes(2))
}
}
}
};
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics = { histogramMetric }
}
}
}
});
Assert.Equal(1, addContext.SuccessCount);
Assert.Equal(1, addContext.FailureCount);
var instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = new ResourceKey("TestService", "TestId"),
MeterName = "test-meter",
InstrumentName = "test",
StartTime = DateTime.MinValue,
EndTime = DateTime.MaxValue
}, cancellationToken: CancellationToken.None);
Assert.NotNull(instrument);
var dimension = Assert.Single(instrument.Dimensions);
var histogramValue = Assert.IsType<HistogramValue>(Assert.Single(dimension.Values));
Assert.Equal([1UL, 2UL, 3UL], histogramValue.Values);
Assert.Equal([1d, 2d], histogramValue.ExplicitBounds);
Assert.Equal(6UL, histogramValue.Count);
}
[Fact]
public async Task AddMetrics_OverflowDimension()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>()
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter", attributes: [KeyValuePair.Create("meter", "value")]),
Metrics =
{
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(1), attributes: [KeyValuePair.Create("otel.metric.overflow", "true")]),
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(2), attributes: [KeyValuePair.Create("dimension", "visible")])
}
},
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter2"),
Metrics =
{
CreateSumMetric(
metricName: "test",
startTime: s_testTime.AddMinutes(1),
attributes:
[
KeyValuePair.Create("otel.metric.overflow", "true"),
KeyValuePair.Create("other", "value")
])
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var instrument1 = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = new ResourceKey("TestService", "TestId"),
InstrumentName = "test",
MeterName = "test-meter",
StartTime = DateTime.MinValue,
EndTime = DateTime.MaxValue,
DimensionFilters = new Dictionary<string, IReadOnlyList<string?>>
{
["dimension"] = ["visible"]
}
}, cancellationToken: CancellationToken.None);
Assert.NotNull(instrument1);
Assert.True(instrument1.HasOverflow);
Assert.Single(instrument1.Dimensions);
var instrument2 = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = new ResourceKey("TestService", "TestId"),
InstrumentName = "test",
MeterName = "test-meter2",
StartTime = DateTime.MinValue,
EndTime = DateTime.MaxValue
}, cancellationToken: CancellationToken.None);
Assert.NotNull(instrument2);
Assert.False(instrument2.HasOverflow);
}
[Fact]
public async Task AddMetrics_NoScope()
{
// Arrange
using var repositoryContext = await CreateRepositoryAsync();
// Act
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>()
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = null,
Metrics =
{
CreateSumMetric(metricName: "test", startTime: s_testTime.AddMinutes(1))
}
}
}
}
});
// Assert
Assert.Equal(0, addContext.FailureCount);
var resources = repositoryContext.Repository.GetResources();
Assert.Collection(resources,
resource =>
{
Assert.Equal("TestService", resource.ResourceName);
Assert.Equal("TestId", resource.InstanceId);
});
var instruments = repositoryContext.Repository.GetInstrumentSummaries(resources[0].ResourceKey);
Assert.Collection(instruments,
instrument =>
{
Assert.Equal("test", instrument.Name);
Assert.Equal("Test metric description", instrument.Description);
Assert.Equal("widget", instrument.Unit);
Assert.Same(OtlpScope.Empty, instrument.Parent);
});
}
[Fact]
public async Task GetInstrument_KnownAttributeValuesIgnoreMergedKeyLimit()
{
const int previousKnownAttributeLimit = 10_000;
var pointAttributeCount = (previousKnownAttributeLimit / 2) + 1;
var scopeAttributeCount = previousKnownAttributeLimit / 2;
using var repositoryContext = await CreateRepositoryAsync(maxAttributeCount: pointAttributeCount);
var pointAttributes = Enumerable.Range(0, pointAttributeCount)
.Select(index => KeyValuePair.Create($"point-key-{index:D5}", $"point-value-{index:D5}"))
.ToArray();
var scopeAttributes = Enumerable.Range(0, scopeAttributeCount)
.Select(index => KeyValuePair.Create($"scope-key-{index:D5}", $"scope-value-{index:D5}"))
.ToArray();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter", attributes: scopeAttributes),
Metrics = { CreateSumMetric(metricName: "test", startTime: s_testTime, attributes: pointAttributes) }
}
}
}
});
Assert.Equal(1, addContext.SuccessCount);
Assert.Equal(0, addContext.FailureCount);
var instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = new ResourceKey("TestService", "TestId"),
MeterName = "test-meter",
InstrumentName = "test",
StartTime = DateTime.MinValue,
EndTime = DateTime.MaxValue
}, cancellationToken: CancellationToken.None);
Assert.Equal(pointAttributeCount + scopeAttributeCount, instrument!.KnownAttributeValues.Count);
Assert.Equal(pointAttributeCount + scopeAttributeCount, Assert.Single(instrument.Dimensions).Attributes.Length);
}
[Fact]
public async Task GetInstrument_KnownAttributeValuesIgnoreMergedPerKeyValueLimit()
{
const int previousKnownAttributeValuesPerKeyLimit = 10_000;
var dimensionCountPerView = (previousKnownAttributeValuesPerKeyLimit / 2) + 1;
using var repositoryContext = await CreateRepositoryAsync();
var resourceMetrics = Enumerable.Range(0, 2)
.Select(viewIndex => new ResourceMetrics
{
Resource = CreateResource(instanceId: $"instance-{viewIndex}"),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
Enumerable.Range(0, dimensionCountPerView)
.Select(dimensionIndex => CreateSumMetric(
metricName: "test",
startTime: s_testTime,
attributes: [KeyValuePair.Create("key", $"value-{viewIndex}-{dimensionIndex:D5}")]))
}
}
}
});
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics> { resourceMetrics });
Assert.Equal(dimensionCountPerView * 2, addContext.SuccessCount);
Assert.Equal(0, addContext.FailureCount);
var instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = new ResourceKey("TestService", null),
MeterName = "test-meter",
InstrumentName = "test",
StartTime = DateTime.MinValue,
EndTime = DateTime.MaxValue
}, cancellationToken: CancellationToken.None);
Assert.Equal(dimensionCountPerView * 2, Assert.Single(instrument!.KnownAttributeValues).Value.Count);
Assert.Equal(dimensionCountPerView * 2, instrument.Dimensions.Count);
}
}
public sealed class SqliteMetricsTests : MetricsTests
{
private static readonly DateTime s_queryTestTime = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
[Fact]
public async Task GetInstrument_PopulateExemplarAttributesFalse_SkipsAttributes()
{
using var repositoryContext = await CreateRepositoryAsync();
var repository = Assert.IsType<SqliteTelemetryRepository>(repositoryContext.Repository);
await repository.AsWriter().AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
CreateResourceMetrics(CreateSumMetric(
metricName: "test",
startTime: s_queryTestTime.AddMinutes(1),
value: 1,
exemplars: [CreateExemplar(s_queryTestTime.AddMinutes(1), 2, [KeyValuePair.Create("key", "value")])]))
});
var activities = new ConcurrentQueue<Activity>();
using var listener = ActivityListenerHelper.Create(repository.SqlActivitySource, onActivityStopped: activities.Enqueue);
using var parent = new Activity("metric exemplar attributes test").Start();
var instrument = await repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = CreateResource().GetResourceKey(),
MeterName = "test-meter",
InstrumentName = "test",
StartTime = DateTime.MinValue,
EndTime = DateTime.MaxValue,
PopulateExemplarAttributes = false
}, cancellationToken: CancellationToken.None);
var exemplar = Assert.Single(Assert.Single(Assert.Single(instrument!.Dimensions).Values).Exemplars);
Assert.Empty(exemplar.Attributes);
var queries = activities
.Where(activity => activity.ParentSpanId == parent.SpanId)
.Select(activity => (string)activity.GetTagItem("db.query.text")!);
Assert.DoesNotContain(queries, query => query.Contains("telemetry_metric_exemplar_attributes", StringComparison.Ordinal));
Assert.Single(queries, query => query.Contains("ranked_metric_points", StringComparison.Ordinal));
}
[Fact]
public async Task GetInstrument_WithoutTimeRange_SkipsMetricPointQueries()
{
using var repositoryContext = await CreateRepositoryAsync();
var repository = Assert.IsType<SqliteTelemetryRepository>(repositoryContext.Repository);
await repository.AsWriter().AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
CreateResourceMetrics(CreateSumMetric("test", s_queryTestTime.AddMinutes(1)))
});
var activities = new ConcurrentQueue<Activity>();
using var listener = ActivityListenerHelper.Create(repository.SqlActivitySource, onActivityStopped: activities.Enqueue);
using var parent = new Activity("metric metadata test").Start();
var instrument = await repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = CreateResource().GetResourceKey(),
MeterName = "test-meter",
InstrumentName = "test"
}, cancellationToken: CancellationToken.None);
Assert.Empty(Assert.Single(instrument!.Dimensions).Values);
var queries = activities
.Where(activity => activity.ParentSpanId == parent.SpanId)
.Select(activity => (string)activity.GetTagItem("db.query.text")!);
Assert.DoesNotContain(queries, query => query.Contains("telemetry_metric_points", StringComparison.Ordinal));
}
[Fact]
public async Task GetInstrument_StaggeredDimensionChanges_ReturnsDimensionTimelines()
{
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
for (var minute = 1; minute <= 3; minute++)
{
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test", startTime: s_queryTestTime.AddMinutes(minute), value: 10, attributes: [KeyValuePair.Create("dimension", "stable")]),
CreateSumMetric(metricName: "test", startTime: s_queryTestTime.AddMinutes(minute), value: 15 + (minute * 5), attributes: [KeyValuePair.Create("dimension", "changing")])
}
}
}
}
});
}
Assert.Equal(0, addContext.FailureCount);
var resource = Assert.Single(repositoryContext.Repository.GetResources());
var instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resource.ResourceKey,
MeterName = "test-meter",
InstrumentName = "test",
StartTime = s_queryTestTime,
EndTime = s_queryTestTime.AddMinutes(4)
}, cancellationToken: CancellationToken.None);
Assert.NotNull(instrument);
var dimensions = instrument.Dimensions.ToDictionary(dimension => Assert.Single(dimension.Attributes).Value);
var stableValue = Assert.IsType<MetricValue<long>>(Assert.Single(dimensions["stable"].Values));
Assert.Equal(10, stableValue.Value);
Assert.Collection(
dimensions["changing"].Values.Cast<MetricValue<long>>(),
value => Assert.Equal(25, value.Value),
value => Assert.Equal(30, value.Value));
}
[Fact]
public async Task GetHistogram_StaggeredDimensionChanges_ReturnsDimensionTimelines()
{
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
for (var minute = 1; minute <= 3; minute++)
{
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateTestHistogramMetric(startTime: s_queryTestTime.AddMinutes(minute), value: 10, dimension: "stable"),
CreateTestHistogramMetric(startTime: s_queryTestTime.AddMinutes(minute), value: 15 + (minute * 5), dimension: "changing")
}
}
}
}
});
}
Assert.Equal(0, addContext.FailureCount);
var resource = Assert.Single(repositoryContext.Repository.GetResources());
var instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resource.ResourceKey,
MeterName = "test-meter",
InstrumentName = "test",
StartTime = s_queryTestTime,
EndTime = s_queryTestTime.AddMinutes(4)
}, cancellationToken: CancellationToken.None);
Assert.NotNull(instrument);
var dimensions = instrument.Dimensions.ToDictionary(dimension => Assert.Single(dimension.Attributes).Value);
var stableValue = Assert.IsType<HistogramValue>(Assert.Single(dimensions["stable"].Values));
Assert.Equal(10ul, stableValue.Count);
Assert.Equal(10ul, stableValue.Values[0]);
Assert.Collection(
dimensions["changing"].Values.Cast<HistogramValue>(),
value =>
{
Assert.Equal(20ul, value.Count);
Assert.Equal(20ul, value.Values[0]);
},
value =>
{
Assert.Equal(25ul, value.Count);
Assert.Equal(25ul, value.Values[0]);
},
value =>
{
Assert.Equal(30ul, value.Count);
Assert.Equal(30ul, value.Values[0]);
});
static Metric CreateTestHistogramMetric(DateTime startTime, int value, string dimension)
{
var metric = CreateHistogramMetric("test", startTime);
var point = Assert.Single(metric.Histogram.DataPoints);
point.Count = checked((ulong)value);
point.Sum = value;
point.BucketCounts.Clear();
point.BucketCounts.AddRange([checked((ulong)value), 0, 0, 0]);
point.Attributes.Add(new KeyValue
{
Key = "dimension",
Value = new AnyValue { StringValue = dimension }
});
return metric;
}
}
[Fact]
public async Task GetInstrument_DimensionCursor_ReturnsExtendedLatestPoint()
{
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(
metricName: "test",
startTime: s_queryTestTime.AddMinutes(1),
value: 10,
attributes: [KeyValuePair.Create("dimension", "one")],
exemplars: [CreateExemplar(s_queryTestTime.AddMinutes(1), 10)])
}
}
}
}
});
var resource = Assert.Single(repositoryContext.Repository.GetResources());
var initialInstrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resource.ResourceKey,
MeterName = "test-meter",
InstrumentName = "test",
StartTime = s_queryTestTime,
EndTime = s_queryTestTime.AddMinutes(1)
}, cancellationToken: CancellationToken.None);
var initialDimension = Assert.Single(initialInstrument!.Dimensions);
var initialValue = Assert.IsType<MetricValue<long>>(Assert.Single(initialDimension.Values));
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(
metricName: "test",
startTime: s_queryTestTime.AddMinutes(2),
value: 10,
attributes: [KeyValuePair.Create("dimension", "one")],
exemplars: [CreateExemplar(s_queryTestTime.AddMinutes(2), 20)])
}
}
}
}
});
var refreshedInstrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resource.ResourceKey,
MeterName = "test-meter",
InstrumentName = "test",
StartTime = s_queryTestTime,
EndTime = s_queryTestTime.AddMinutes(2),
DimensionCursors =
[
new MetricDimensionCursor
{
Attributes = initialDimension.Attributes,
StartTime = s_queryTestTime.AddMinutes(1.5)
}
]
}, cancellationToken: CancellationToken.None);
Assert.Equal(0, addContext.FailureCount);
var refreshedValue = Assert.IsType<MetricValue<long>>(Assert.Single(Assert.Single(refreshedInstrument!.Dimensions).Values));
Assert.Equal(initialValue.Start, refreshedValue.Start);
Assert.Equal(s_queryTestTime.AddMinutes(2), refreshedValue.End);
Assert.Equal(2ul, refreshedValue.Count);
Assert.Equal(20, Assert.Single(refreshedValue.Exemplars).Value);
}
[Fact]
public async Task GetInstrument_DataPointInterval_RollsUpNumericValuesAndExemplars()
{
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(
metricName: "test",
startTime: s_queryTestTime.AddMilliseconds(100),
value: 3,
exemplars: [CreateExemplar(s_queryTestTime.AddMilliseconds(100), 3)]),
CreateSumMetric(metricName: "test", startTime: s_queryTestTime.AddMilliseconds(200), value: 2),
CreateSumMetric(metricName: "test", startTime: s_queryTestTime.AddMilliseconds(800), value: 1)
}
}
}
}
});
var resource = Assert.Single(repositoryContext.Repository.GetResources());
var instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resource.ResourceKey,
MeterName = "test-meter",
InstrumentName = "test",
StartTime = s_queryTestTime,
EndTime = s_queryTestTime.AddSeconds(1),
DataPointInterval = TimeSpan.FromSeconds(1)
}, cancellationToken: CancellationToken.None);
Assert.Equal(0, addContext.FailureCount);
var value = Assert.IsType<MetricValue<long>>(Assert.Single(Assert.Single(instrument!.Dimensions).Values));
Assert.Equal(2, value.Value);
Assert.Equal(s_queryTestTime, value.Start);
var exemplar = Assert.Single(value.Exemplars);
Assert.Equal(3, exemplar.Value);
}
[Fact]
public async Task GetInstrument_IncrementalRollup_RecomputesCompleteLatestBucket()
{
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
await AddMetric(s_queryTestTime.AddSeconds(1), 5);
await AddMetric(s_queryTestTime.AddSeconds(5), 5);
await AddMetric(s_queryTestTime.AddSeconds(10), 3);
await AddMetric(s_queryTestTime.AddMinutes(2), 3);
var resource = Assert.Single(repositoryContext.Repository.GetResources());
var initialInstrument = await GetInstrumentAsync([]);
var initialValue = Assert.IsType<MetricValue<long>>(Assert.Single(Assert.Single(initialInstrument.Dimensions).Values));
var cursors = MetricInstrumentDataCache.CreateCursors(initialInstrument, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
var cursor = Assert.Single(cursors);
Assert.Equal(0, addContext.FailureCount);
Assert.Equal(5, initialValue.Value);
Assert.Equal(s_queryTestTime, cursor.StartTime);
var refreshedInstrument = await GetInstrumentAsync(cursors);
var refreshedValue = Assert.IsType<MetricValue<long>>(Assert.Single(Assert.Single(refreshedInstrument.Dimensions).Values));
Assert.Equal(initialValue.Value, refreshedValue.Value);
Assert.Equal(initialValue.Count, refreshedValue.Count);
Assert.Equal(initialValue.Start, refreshedValue.Start);
Assert.Equal(initialValue.End, refreshedValue.End);
async Task AddMetric(DateTime startTime, int value)
{
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics = { CreateSumMetric(metricName: "test", startTime: startTime, value: value) }
}
}
}
});
}
async Task<OtlpInstrumentData> GetInstrumentAsync(IReadOnlyList<MetricDimensionCursor> dimensionCursors)
{
return (await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resource.ResourceKey,
MeterName = "test-meter",
InstrumentName = "test",
StartTime = s_queryTestTime,
EndTime = s_queryTestTime.AddMinutes(2),
DataPointInterval = TimeSpan.FromMinutes(1),
DimensionCursors = dimensionCursors
}, cancellationToken: CancellationToken.None))!;
}
}
[Fact]
public async Task GetHistogram_DataPointInterval_ReturnsLatestCoherentSnapshot()
{
using var repositoryContext = await CreateRepositoryAsync();
var addContext = new AddContext();
var metrics = new List<Metric>();
for (var pointIndex = 1; pointIndex <= 3; pointIndex++)
{
var metric = CreateHistogramMetric("test", s_queryTestTime.AddMilliseconds(pointIndex * 100));
var point = Assert.Single(metric.Histogram.DataPoints);
point.Count = checked((ulong)pointIndex);
point.Sum = pointIndex * 10;
point.BucketCounts.Clear();
point.BucketCounts.AddRange(pointIndex switch
{
1 => [1, 0, 0, 0],
2 => [1, 1, 0, 0],
_ => [1, 1, 1, 0]
});
if (pointIndex == 2)
{
point.Exemplars.Add(CreateExemplar(s_queryTestTime.AddMilliseconds(200), 20));
}
metrics.Add(metric);
}
await repositoryContext.Repository.AsWriter().AddMetricsAsync(addContext, new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics = { metrics }
}
}
}
});
var resource = Assert.Single(repositoryContext.Repository.GetResources());
var instrument = await repositoryContext.Repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resource.ResourceKey,
MeterName = "test-meter",
InstrumentName = "test",
StartTime = s_queryTestTime,
EndTime = s_queryTestTime.AddMinutes(1),
DataPointInterval = TimeSpan.FromMinutes(1)
}, cancellationToken: CancellationToken.None);
Assert.Equal(0, addContext.FailureCount);
var value = Assert.IsType<HistogramValue>(Assert.Single(Assert.Single(instrument!.Dimensions).Values));
Assert.Equal(3ul, value.Count);
Assert.Equal(30, value.Sum);
Assert.Equal([1ul, 1ul, 1ul, 0ul], value.Values);
Assert.Equal(20, Assert.Single(value.Exemplars).Value);
}
[Fact]
public async Task AddMetrics_ReusesInstrumentAndDimensionLookupsWithinBatch()
{
using var repositoryContext = await CreateRepositoryAsync();
var repository = Assert.IsType<SqliteTelemetryRepository>(repositoryContext.Repository);
var activities = new ConcurrentQueue<Activity>();
using var listener = ActivityListenerHelper.Create(repository.SqlActivitySource, onActivityStopped: activities.Enqueue);
using var parent = new Activity("metric ingestion test").Start();
var context = new AddContext();
await repository.AsWriter().AddMetricsAsync(context, new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test", startTime: s_queryTestTime.AddMinutes(1), value: 1),
CreateSumMetric(metricName: "test", startTime: s_queryTestTime.AddMinutes(2), value: 2),
CreateSumMetric(metricName: "test", startTime: s_queryTestTime.AddMinutes(3), value: 2)
}
}
}
}
});
var queries = activities
.Where(activity => activity.ParentSpanId == parent.SpanId)
.Select(activity => (string)activity.GetTagItem("db.query.text")!)
.ToList();
Assert.Single(queries, query => query.Contains("SELECT instrument_id", StringComparison.Ordinal));
Assert.DoesNotContain(queries, query => query.Contains("FROM telemetry_metric_dimensions d", StringComparison.Ordinal));
Assert.Single(queries, query => query.StartsWith("DELETE FROM telemetry_metric_points", StringComparison.Ordinal));
var insertQuery = Assert.Single(queries, query => query.StartsWith("INSERT INTO telemetry_metric_points", StringComparison.Ordinal));
Assert.Equal(2, insertQuery.Split("@param_dimension_id_", StringSplitOptions.None).Length - 1);
Assert.Equal(3, context.SuccessCount);
}
[Fact]
public async Task AddMetrics_LargeHistogramAndDimensionAttributeBatchesRoundTrip()
{
using var repositoryContext = await CreateRepositoryAsync();
var repository = Assert.IsType<SqliteTelemetryRepository>(repositoryContext.Repository);
var histogram = CreateHistogramMetric(metricName: "histogram", startTime: s_queryTestTime.AddMinutes(1));
var histogramPoint = histogram.Histogram.DataPoints[0];
histogramPoint.ExplicitBounds.Clear();
histogramPoint.BucketCounts.Clear();
for (var index = 0; index < 200; index++)
{
histogramPoint.ExplicitBounds.Add(index + 1);
histogramPoint.BucketCounts.Add(1);
}
histogramPoint.BucketCounts.Add(1);
var firstDimensionAttributes = Enumerable.Range(0, 128)
.Select(index => KeyValuePair.Create($"key-{index}", $"first-{index}"))
.ToArray();
var secondDimensionAttributes = Enumerable.Range(0, 128)
.Select(index => KeyValuePair.Create($"key-{index}", $"second-{index}"))
.ToArray();
var context = new AddContext();
await repository.AsWriter().AddMetricsAsync(context, new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
histogram,
CreateSumMetric(metricName: "sum", startTime: s_queryTestTime.AddMinutes(1), attributes: firstDimensionAttributes),
CreateSumMetric(metricName: "sum", startTime: s_queryTestTime.AddMinutes(1), attributes: secondDimensionAttributes)
}
}
}
}
});
Assert.Equal(3, context.SuccessCount);
Assert.Equal(0, context.FailureCount);
var resourceKey = CreateResource().GetResourceKey();
var histogramInstrument = await repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resourceKey,
MeterName = "test-meter",
InstrumentName = "histogram",
StartTime = DateTime.MinValue,
EndTime = DateTime.MaxValue
}, cancellationToken: CancellationToken.None);
var histogramValue = Assert.IsType<HistogramValue>(Assert.Single(Assert.Single(histogramInstrument!.Dimensions).Values));
Assert.Equal(Enumerable.Repeat<ulong>(1, 201), histogramValue.Values);
Assert.Equal(Enumerable.Range(1, 200).Select(value => (double)value), histogramValue.ExplicitBounds);
var sumInstrument = await repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = resourceKey,
MeterName = "test-meter",
InstrumentName = "sum",
StartTime = DateTime.MinValue,
EndTime = DateTime.MaxValue
}, cancellationToken: CancellationToken.None);
Assert.Collection(
sumInstrument!.Dimensions,
dimension =>
{
Assert.Equal(firstDimensionAttributes.OrderBy(attribute => attribute.Key), dimension.Attributes);
Assert.Equal(1, Assert.IsType<MetricValue<long>>(Assert.Single(dimension.Values)).Value);
},
dimension =>
{
Assert.Equal(secondDimensionAttributes.OrderBy(attribute => attribute.Key), dimension.Attributes);
Assert.Equal(1, Assert.IsType<MetricValue<long>>(Assert.Single(dimension.Values)).Value);
});
}
[Fact]
public async Task AddMetrics_BatchesAndDeduplicatesExemplars()
{
using var repositoryContext = await CreateRepositoryAsync();
var repository = Assert.IsType<SqliteTelemetryRepository>(repositoryContext.Repository);
await repository.AsWriter().AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
CreateResourceMetrics(CreateSumMetric(
metricName: "test",
startTime: s_queryTestTime.AddMinutes(1),
value: 1,
exemplars: [CreateExemplar(s_queryTestTime.AddMinutes(1), 2, [KeyValuePair.Create("first", "value")])]))
});
var activities = new ConcurrentQueue<Activity>();
using var listener = ActivityListenerHelper.Create(repository.SqlActivitySource, onActivityStopped: activities.Enqueue);
using var parent = new Activity("metric exemplar test").Start();
var context = new AddContext();
await repository.AsWriter().AddMetricsAsync(context, new RepeatedField<ResourceMetrics>
{
CreateResourceMetrics(CreateSumMetric(
metricName: "test",
startTime: s_queryTestTime.AddMinutes(2),
value: 1,
exemplars:
[
CreateExemplar(s_queryTestTime.AddMinutes(1), 2, [KeyValuePair.Create("first", "value")]),
CreateExemplar(s_queryTestTime.AddMinutes(2), 3, [KeyValuePair.Create("second", "value")])
]))
});
var queries = activities
.Where(activity => activity.ParentSpanId == parent.SpanId)
.Select(activity => (string)activity.GetTagItem("db.query.text")!)
.ToList();
Assert.DoesNotContain(queries, query => query.Contains("SELECT EXISTS", StringComparison.Ordinal));
var exemplarInsert = Assert.Single(queries, query => query.StartsWith("INSERT OR IGNORE INTO telemetry_metric_exemplars", StringComparison.Ordinal));
Assert.Equal(2, exemplarInsert.Split("@PointId", StringSplitOptions.None).Length - 1);
Assert.Single(queries, query => query.StartsWith("INSERT INTO telemetry_metric_exemplar_attributes", StringComparison.Ordinal));
Assert.Equal(1, context.SuccessCount);
Assert.Equal(0, context.FailureCount);
var instrument = await repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = CreateResource().GetResourceKey(),
MeterName = "test-meter",
InstrumentName = "test",
StartTime = DateTime.MinValue,
EndTime = DateTime.MaxValue
}, cancellationToken: CancellationToken.None);
var value = Assert.IsType<MetricValue<long>>(Assert.Single(Assert.Single(instrument!.Dimensions).Values));
Assert.Collection(value.Exemplars,
exemplar => Assert.Equal("first", Assert.Single(exemplar.Attributes).Key),
exemplar => Assert.Equal("second", Assert.Single(exemplar.Attributes).Key));
}
[Fact]
public async Task AddMetrics_UpdateOnlyBatch_DoesNotTrimMetricPoints()
{
using var repositoryContext = await CreateRepositoryAsync();
var repository = Assert.IsType<SqliteTelemetryRepository>(repositoryContext.Repository);
await repository.AsWriter().AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
CreateResourceMetrics(CreateSumMetric(metricName: "test", startTime: s_queryTestTime.AddMinutes(1), value: 1))
});
var activities = new ConcurrentQueue<Activity>();
using var listener = ActivityListenerHelper.Create(repository.SqlActivitySource, onActivityStopped: activities.Enqueue);
using var parent = new Activity("metric update test").Start();
await repository.AsWriter().AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
new ResourceMetrics
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics =
{
CreateSumMetric(metricName: "test", startTime: s_queryTestTime.AddMinutes(2), value: 1),
CreateSumMetric(metricName: "test", startTime: s_queryTestTime.AddMinutes(3), value: 1),
CreateSumMetric(metricName: "test", startTime: s_queryTestTime.AddMinutes(4), value: 1)
}
}
}
}
});
var queries = activities
.Where(activity => activity.ParentSpanId == parent.SpanId)
.Select(activity => (string)activity.GetTagItem("db.query.text")!)
.ToList();
Assert.DoesNotContain(queries, query => query.Contains("SELECT resource_id", StringComparison.Ordinal));
Assert.DoesNotContain(queries, query => query.Contains("FROM telemetry_scopes", StringComparison.Ordinal));
Assert.DoesNotContain(queries, query => query.Contains("FROM telemetry_scope_attributes", StringComparison.Ordinal));
Assert.DoesNotContain(queries, query => query.Contains("SELECT instrument_id", StringComparison.Ordinal));
Assert.DoesNotContain(queries, query => query.Contains("FROM telemetry_metric_dimensions d", StringComparison.Ordinal));
Assert.DoesNotContain(queries, query => query.StartsWith("UPDATE telemetry_resources SET has_metrics", StringComparison.Ordinal));
Assert.DoesNotContain(queries, query => query.StartsWith("DELETE FROM telemetry_metric_points", StringComparison.Ordinal));
var updateQuery = Assert.Single(queries, query => query.Contains("UPDATE telemetry_metric_points", StringComparison.Ordinal));
Assert.Contains("WITH updates", updateQuery, StringComparison.Ordinal);
Assert.Contains("FROM updates", updateQuery, StringComparison.Ordinal);
Assert.DoesNotContain("SELECT end_time_ticks FROM updates", updateQuery, StringComparison.Ordinal);
var instrument = await repository.GetInstrumentAsync(new GetInstrumentRequest
{
ResourceKey = CreateResource().GetResourceKey(),
MeterName = "test-meter",
InstrumentName = "test",
StartTime = DateTime.MinValue,
EndTime = DateTime.MaxValue
}, cancellationToken: CancellationToken.None);
var value = Assert.IsType<MetricValue<long>>(Assert.Single(Assert.Single(instrument!.Dimensions).Values));
Assert.Equal(4UL, value.Count);
Assert.Equal(s_queryTestTime.AddMinutes(4), value.End);
}
[Fact]
public async Task ClearMetrics_InvalidatesMetricIngestionCache()
{
using var repositoryContext = await CreateRepositoryAsync();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(new AddContext(), new RepeatedField<ResourceMetrics>
{
CreateResourceMetrics(CreateSumMetric(metricName: "test", startTime: s_queryTestTime.AddMinutes(1), value: 1))
});
await repositoryContext.Repository.AsWriter().ClearMetricsAsync();
var context = new AddContext();
await repositoryContext.Repository.AsWriter().AddMetricsAsync(context, new RepeatedField<ResourceMetrics>
{
CreateResourceMetrics(CreateSumMetric(metricName: "test", startTime: s_queryTestTime.AddMinutes(2), value: 2))
});
Assert.Equal(1, context.SuccessCount);
Assert.Equal(0, context.FailureCount);
Assert.Single(repositoryContext.Repository.GetInstrumentSummaries(CreateResource().GetResourceKey()));
}
private static ResourceMetrics CreateResourceMetrics(Metric metric) => new()
{
Resource = CreateResource(),
ScopeMetrics =
{
new ScopeMetrics
{
Scope = CreateScope(name: "test-meter"),
Metrics = { metric }
}
}
};
}