Add support for ГОСТ Р 34.10 digital signatures

- Implemented the GostKeyValue class for handling public key parameters in ГОСТ Р 34.10 digital signatures.
- Created the GostSignedXml class to manage XML signatures using ГОСТ 34.10, including methods for computing and checking signatures.
- Developed the GostSignedXmlImpl class to encapsulate the signature computation logic and public key retrieval.
- Added specific key value classes for ГОСТ Р 34.10-2001, ГОСТ Р 34.10-2012/256, and ГОСТ Р 34.10-2012/512 to support different signature algorithms.
- Ensured compatibility with existing XML signature standards while integrating ГОСТ cryptography.
This commit is contained in:
master
2025-11-09 21:59:57 +02:00
parent 75c2bcafce
commit cef4cb2c5a
486 changed files with 32952 additions and 801 deletions

View File

@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Diagnostics.Metrics;
namespace StellaOps.Concelier.WebService.Diagnostics;
internal static class AdvisoryAiMetrics
{
internal const string MeterName = "StellaOps.Concelier.WebService.AdvisoryAi";
private static readonly Meter Meter = new(MeterName);
internal static readonly Counter<long> ChunkRequestCounter = Meter.CreateCounter<long>(
"advisory_ai_chunk_requests_total",
unit: "count",
description: "Number of advisory chunk requests processed by the web service.");
internal static readonly Counter<long> ChunkCacheHitCounter = Meter.CreateCounter<long>(
"advisory_ai_chunk_cache_hits_total",
unit: "count",
description: "Number of advisory chunk requests served from cache.");
internal static readonly Counter<long> GuardrailBlockCounter = Meter.CreateCounter<long>(
"advisory_ai_guardrail_blocks_total",
unit: "count",
description: "Number of advisory chunk segments blocked by guardrails.");
internal static KeyValuePair<string, object?>[] BuildChunkRequestTags(string tenant, string result, bool truncated, bool cacheHit)
=> new[]
{
CreateTag("tenant", tenant),
CreateTag("result", result),
CreateTag("truncated", BoolToString(truncated)),
CreateTag("cache", cacheHit ? "hit" : "miss"),
};
internal static KeyValuePair<string, object?>[] BuildCacheTags(string tenant, string outcome)
=> new[]
{
CreateTag("tenant", tenant),
CreateTag("result", outcome),
};
internal static KeyValuePair<string, object?>[] BuildGuardrailTags(string tenant, string reason, bool fromCache)
=> new[]
{
CreateTag("tenant", tenant),
CreateTag("reason", reason),
CreateTag("cache", fromCache ? "hit" : "miss"),
};
private static KeyValuePair<string, object?> CreateTag(string key, object? value)
=> new(key, value);
private static string BoolToString(bool value) => value ? "true" : "false";
}

View File

@@ -169,5 +169,7 @@ public sealed class ConcelierOptions
public int DefaultMinimumLength { get; set; } = 64;
public int MaxMinimumLength { get; set; } = 512;
public int CacheDurationSeconds { get; set; } = 30;
}
}

View File

@@ -306,5 +306,10 @@ public static class ConcelierOptionsValidator
{
throw new InvalidOperationException("Advisory chunk maxMinimumLength must be greater than or equal to defaultMinimumLength.");
}
if (chunks.CacheDurationSeconds < 0)
{
throw new InvalidOperationException("Advisory chunk cacheDurationSeconds must be greater than or equal to zero.");
}
}
}

View File

@@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -105,6 +106,8 @@ builder.Services.AddConcelierLinksetMappers();
builder.Services.AddAdvisoryRawServices();
builder.Services.AddSingleton<IAdvisoryObservationQueryService, AdvisoryObservationQueryService>();
builder.Services.AddSingleton<AdvisoryChunkBuilder>();
builder.Services.AddSingleton<IAdvisoryChunkCache, AdvisoryChunkCache>();
builder.Services.AddSingleton<IAdvisoryAiTelemetry, AdvisoryAiTelemetry>();
var features = concelierOptions.Features ?? new ConcelierOptions.FeaturesOptions();
@@ -808,23 +811,37 @@ var advisoryChunksEndpoint = app.MapGet("/advisories/{advisoryKey}/chunks", asyn
HttpContext context,
[FromServices] IAdvisoryObservationQueryService observationService,
[FromServices] AdvisoryChunkBuilder chunkBuilder,
[FromServices] IAdvisoryChunkCache chunkCache,
[FromServices] IAdvisoryAiTelemetry telemetry,
[FromServices] TimeProvider timeProvider,
CancellationToken cancellationToken) =>
{
ApplyNoCache(context.Response);
var requestStart = timeProvider.GetTimestamp();
if (!TryResolveTenant(context, requireHeader: false, out var tenant, out var tenantError))
{
telemetry.TrackChunkFailure(null, advisoryKey ?? string.Empty, "tenant_unresolved", "validation_error");
return tenantError;
}
var authorizationError = EnsureTenantAuthorized(context, tenant);
if (authorizationError is not null)
{
var failureResult = authorizationError switch
{
UnauthorizedHttpResult => "unauthorized",
_ => "forbidden"
};
telemetry.TrackChunkFailure(tenant, advisoryKey ?? string.Empty, "tenant_not_authorized", failureResult);
return authorizationError;
}
if (string.IsNullOrWhiteSpace(advisoryKey))
{
telemetry.TrackChunkFailure(tenant, string.Empty, "missing_key", "validation_error");
return Problem(context, "advisoryKey is required", StatusCodes.Status400BadRequest, ProblemTypes.Validation, "Provide an advisory identifier.");
}
@@ -845,9 +862,11 @@ var advisoryChunksEndpoint = app.MapGet("/advisories/{advisoryKey}/chunks", asyn
var observationResult = await observationService.QueryAsync(queryOptions, cancellationToken).ConfigureAwait(false);
if (observationResult.Observations.IsDefaultOrEmpty || observationResult.Observations.Length == 0)
{
telemetry.TrackChunkFailure(tenant, normalizedKey, "advisory_not_found", "not_found");
return Problem(context, "Advisory not found", StatusCodes.Status404NotFound, ProblemTypes.NotFound, $"No observations available for {normalizedKey}.");
}
var observations = observationResult.Observations.ToArray();
var buildOptions = new AdvisoryChunkBuildOptions(
normalizedKey,
chunkLimit,
@@ -856,9 +875,51 @@ var advisoryChunksEndpoint = app.MapGet("/advisories/{advisoryKey}/chunks", asyn
formatFilter,
minimumLength);
var response = chunkBuilder.Build(buildOptions, observationResult.Observations.ToArray());
return JsonResult(response);
var cacheDuration = chunkSettings.CacheDurationSeconds > 0
? TimeSpan.FromSeconds(chunkSettings.CacheDurationSeconds)
: TimeSpan.Zero;
AdvisoryChunkBuildResult buildResult;
var cacheHit = false;
if (cacheDuration > TimeSpan.Zero)
{
var cacheKey = AdvisoryChunkCacheKey.Create(tenant, normalizedKey, buildOptions, observations);
if (chunkCache.TryGet(cacheKey, out var cachedResult))
{
buildResult = cachedResult;
cacheHit = true;
}
else
{
buildResult = chunkBuilder.Build(buildOptions, observations);
chunkCache.Set(cacheKey, buildResult, cacheDuration);
}
}
else
{
buildResult = chunkBuilder.Build(buildOptions, observations);
}
var duration = timeProvider.GetElapsedTime(requestStart);
var guardrailCounts = cacheHit
? ImmutableDictionary<AdvisoryChunkGuardrailReason, int>.Empty
: buildResult.Telemetry.GuardrailCounts;
telemetry.TrackChunkResult(new AdvisoryAiChunkRequestTelemetry(
tenant,
normalizedKey,
"ok",
buildResult.Response.Truncated,
cacheHit,
observations.Length,
buildResult.Response.Chunks.Count,
duration,
guardrailCounts));
return JsonResult(buildResult.Response);
});
if (authorityConfigured)
{
advisoryChunksEndpoint.RequireAuthorization(AdvisoryReadPolicyName);

View File

@@ -0,0 +1,128 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using Microsoft.Extensions.Logging;
using StellaOps.Concelier.WebService.Diagnostics;
namespace StellaOps.Concelier.WebService.Services;
internal interface IAdvisoryAiTelemetry
{
void TrackChunkResult(AdvisoryAiChunkRequestTelemetry telemetry);
void TrackChunkFailure(string? tenant, string advisoryKey, string failureReason, string result);
}
internal sealed class AdvisoryAiTelemetry : IAdvisoryAiTelemetry
{
private readonly ILogger<AdvisoryAiTelemetry> _logger;
public AdvisoryAiTelemetry(ILogger<AdvisoryAiTelemetry> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public void TrackChunkResult(AdvisoryAiChunkRequestTelemetry telemetry)
{
ArgumentNullException.ThrowIfNull(telemetry);
var tenant = NormalizeTenant(telemetry.Tenant);
var result = NormalizeResult(telemetry.Result);
AdvisoryAiMetrics.ChunkRequestCounter.Add(1,
AdvisoryAiMetrics.BuildChunkRequestTags(tenant, result, telemetry.Truncated, telemetry.CacheHit));
if (telemetry.CacheHit)
{
AdvisoryAiMetrics.ChunkCacheHitCounter.Add(1,
AdvisoryAiMetrics.BuildCacheTags(tenant, "hit"));
}
if (!telemetry.CacheHit && telemetry.GuardrailCounts.Count > 0)
{
foreach (var kvp in telemetry.GuardrailCounts)
{
AdvisoryAiMetrics.GuardrailBlockCounter.Add(kvp.Value,
AdvisoryAiMetrics.BuildGuardrailTags(tenant, GetReasonTag(kvp.Key), telemetry.CacheHit));
}
_logger.LogInformation(
"Advisory chunk guardrails blocked {BlockCount} segments for tenant {Tenant} and key {Key}. Details: {Summary}",
telemetry.TotalGuardrailBlocks,
tenant,
telemetry.AdvisoryKey,
FormatGuardrailSummary(telemetry.GuardrailCounts));
}
_logger.LogInformation(
"Advisory chunk request for tenant {Tenant} key {Key} returned {Chunks} chunks across {Sources} sources (truncated: {Truncated}, cacheHit: {CacheHit}, durationMs: {Duration}).",
tenant,
telemetry.AdvisoryKey,
telemetry.ChunkCount,
telemetry.ObservationCount,
telemetry.Truncated,
telemetry.CacheHit,
telemetry.Duration.TotalMilliseconds.ToString("F2", CultureInfo.InvariantCulture));
}
public void TrackChunkFailure(string? tenant, string advisoryKey, string failureReason, string result)
{
var normalizedTenant = NormalizeTenant(tenant);
var normalizedResult = NormalizeResult(result);
AdvisoryAiMetrics.ChunkRequestCounter.Add(1,
AdvisoryAiMetrics.BuildChunkRequestTags(normalizedTenant, normalizedResult, truncated: false, cacheHit: false));
_logger.LogWarning(
"Advisory chunk request for tenant {Tenant} key {Key} failed ({Result}): {Reason}",
normalizedTenant,
advisoryKey,
normalizedResult,
failureReason);
}
private static string NormalizeTenant(string? tenant)
=> string.IsNullOrWhiteSpace(tenant) ? "unknown" : tenant;
private static string NormalizeResult(string? result)
=> string.IsNullOrWhiteSpace(result) ? "unknown" : result;
private static string GetReasonTag(AdvisoryChunkGuardrailReason reason)
=> reason switch
{
AdvisoryChunkGuardrailReason.NormalizationFailed => "normalization_failed",
AdvisoryChunkGuardrailReason.BelowMinimumLength => "below_minimum_length",
AdvisoryChunkGuardrailReason.MissingAlphabeticCharacters => "missing_alpha_characters",
_ => reason.ToString().ToLowerInvariant()
};
private static string FormatGuardrailSummary(IReadOnlyDictionary<AdvisoryChunkGuardrailReason, int> counts)
{
if (counts.Count == 0)
{
return "none";
}
var parts = counts
.OrderBy(static kvp => kvp.Key)
.Select(kvp => $"{GetReasonTag(kvp.Key)}={kvp.Value}");
return string.Join(",", parts);
}
}
internal sealed record AdvisoryAiChunkRequestTelemetry(
string? Tenant,
string AdvisoryKey,
string Result,
bool Truncated,
bool CacheHit,
int ObservationCount,
int ChunkCount,
TimeSpan Duration,
IReadOnlyDictionary<AdvisoryChunkGuardrailReason, int> GuardrailCounts)
{
public int TotalGuardrailBlocks => GuardrailCounts.Count == 0
? 0
: GuardrailCounts.Values.Sum();
}

View File

@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
@@ -27,7 +29,7 @@ internal sealed class AdvisoryChunkBuilder
_hash = hash ?? throw new ArgumentNullException(nameof(hash));
}
public AdvisoryChunkCollectionResponse Build(
public AdvisoryChunkBuildResult Build(
AdvisoryChunkBuildOptions options,
IReadOnlyList<AdvisoryObservation> observations)
{
@@ -35,6 +37,7 @@ internal sealed class AdvisoryChunkBuilder
var sources = new List<AdvisoryChunkSourceResponse>();
var total = 0;
var truncated = false;
var guardrailCounts = new Dictionary<AdvisoryChunkGuardrailReason, int>();
foreach (var observation in observations
.OrderByDescending(o => o.CreatedAt))
@@ -60,7 +63,7 @@ internal sealed class AdvisoryChunkBuilder
observation.Upstream.ContentHash,
observation.CreatedAt));
foreach (var chunk in ExtractChunks(observation, documentId, options))
foreach (var chunk in ExtractChunks(observation, documentId, options, guardrailCounts))
{
total++;
if (chunks.Count < options.ChunkLimit)
@@ -85,12 +88,23 @@ internal sealed class AdvisoryChunkBuilder
total = chunks.Count;
}
return new AdvisoryChunkCollectionResponse(
var response = new AdvisoryChunkCollectionResponse(
options.AdvisoryKey,
total,
truncated,
chunks,
sources);
var guardrailSnapshot = guardrailCounts.Count == 0
? ImmutableDictionary<AdvisoryChunkGuardrailReason, int>.Empty
: guardrailCounts.ToImmutableDictionary();
var telemetry = new AdvisoryChunkTelemetrySummary(
sources.Count,
truncated,
guardrailSnapshot);
return new AdvisoryChunkBuildResult(response, telemetry);
}
private static string DetermineDocumentId(AdvisoryObservation observation)
@@ -106,7 +120,8 @@ internal sealed class AdvisoryChunkBuilder
private IEnumerable<AdvisoryChunkItemResponse> ExtractChunks(
AdvisoryObservation observation,
string documentId,
AdvisoryChunkBuildOptions options)
AdvisoryChunkBuildOptions options,
IDictionary<AdvisoryChunkGuardrailReason, int> guardrailCounts)
{
var root = observation.Content.Raw;
if (root is null)
@@ -127,21 +142,29 @@ internal sealed class AdvisoryChunkBuilder
switch (node)
{
case JsonValue value when TryNormalize(value, out var text):
case JsonValue value:
if (!TryNormalize(value, out var text))
{
IncrementGuardrailCount(guardrailCounts, AdvisoryChunkGuardrailReason.NormalizationFailed);
break;
}
if (text.Length < Math.Max(options.MinimumLength, DefaultMinLength))
{
continue;
IncrementGuardrailCount(guardrailCounts, AdvisoryChunkGuardrailReason.BelowMinimumLength);
break;
}
if (!ContainsLetter(text))
{
continue;
IncrementGuardrailCount(guardrailCounts, AdvisoryChunkGuardrailReason.MissingAlphabeticCharacters);
break;
}
var resolvedSection = string.IsNullOrEmpty(section) ? documentId : section;
if (options.SectionFilter.Count > 0 && !options.SectionFilter.Contains(resolvedSection))
{
continue;
break;
}
var paragraphId = string.IsNullOrEmpty(path) ? resolvedSection : path;
@@ -195,6 +218,7 @@ internal sealed class AdvisoryChunkBuilder
}
}
private static bool TryNormalize(JsonValue value, out string normalized)
{
normalized = string.Empty;
@@ -260,4 +284,37 @@ internal sealed class AdvisoryChunkBuilder
var digest = _hash.ComputeHash(Encoding.UTF8.GetBytes(input), HashAlgorithms.Sha256);
return string.Concat(documentId, ':', Convert.ToHexString(digest.AsSpan(0, 8)));
}
private static void IncrementGuardrailCount(
IDictionary<AdvisoryChunkGuardrailReason, int> counts,
AdvisoryChunkGuardrailReason reason)
{
if (!counts.TryGetValue(reason, out var current))
{
current = 0;
}
counts[reason] = current + 1;
}
}
internal sealed record AdvisoryChunkBuildResult(
AdvisoryChunkCollectionResponse Response,
AdvisoryChunkTelemetrySummary Telemetry);
internal sealed record AdvisoryChunkTelemetrySummary(
int SourceCount,
bool Truncated,
IReadOnlyDictionary<AdvisoryChunkGuardrailReason, int> GuardrailCounts)
{
public int GuardrailBlockCount => GuardrailCounts.Count == 0
? 0
: GuardrailCounts.Values.Sum();
}
internal enum AdvisoryChunkGuardrailReason
{
NormalizationFailed,
BelowMinimumLength,
MissingAlphabeticCharacters
}

View File

@@ -0,0 +1,109 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text;
using Microsoft.Extensions.Caching.Memory;
using StellaOps.Concelier.Models.Observations;
namespace StellaOps.Concelier.WebService.Services;
internal interface IAdvisoryChunkCache
{
bool TryGet(in AdvisoryChunkCacheKey key, out AdvisoryChunkBuildResult result);
void Set(in AdvisoryChunkCacheKey key, AdvisoryChunkBuildResult value, TimeSpan ttl);
}
internal sealed class AdvisoryChunkCache : IAdvisoryChunkCache
{
private readonly IMemoryCache _memoryCache;
public AdvisoryChunkCache(IMemoryCache memoryCache)
{
_memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
}
public bool TryGet(in AdvisoryChunkCacheKey key, out AdvisoryChunkBuildResult result)
{
if (_memoryCache.TryGetValue(key.Value, out AdvisoryChunkBuildResult? cached) && cached is not null)
{
result = cached;
return true;
}
result = null!;
return false;
}
public void Set(in AdvisoryChunkCacheKey key, AdvisoryChunkBuildResult value, TimeSpan ttl)
{
if (ttl <= TimeSpan.Zero)
{
return;
}
_memoryCache.Set(key.Value, value, ttl);
}
}
internal readonly record struct AdvisoryChunkCacheKey(string Value)
{
public static AdvisoryChunkCacheKey Create(
string tenant,
string advisoryKey,
AdvisoryChunkBuildOptions options,
IReadOnlyList<AdvisoryObservation> observations)
{
var builder = new StringBuilder();
builder.Append(tenant);
builder.Append('|');
builder.Append(advisoryKey);
builder.Append('|');
builder.Append(options.ChunkLimit);
builder.Append('|');
builder.Append(options.ObservationLimit);
builder.Append('|');
builder.Append(options.MinimumLength);
builder.Append('|');
AppendSet(builder, options.SectionFilter);
builder.Append('|');
AppendSet(builder, options.FormatFilter);
builder.Append('|');
foreach (var observation in observations
.OrderBy(static o => o.ObservationId, StringComparer.Ordinal))
{
builder.Append(observation.ObservationId);
builder.Append('@');
builder.Append(observation.Upstream?.ContentHash ?? string.Empty);
builder.Append('@');
builder.Append(observation.CreatedAt.UtcDateTime.Ticks.ToString(CultureInfo.InvariantCulture));
builder.Append('@');
builder.Append(observation.Content.Format ?? string.Empty);
builder.Append(';');
}
return new AdvisoryChunkCacheKey(builder.ToString());
}
private static void AppendSet(StringBuilder builder, ImmutableHashSet<string> values)
{
if (values.Count == 0)
{
builder.Append('-');
return;
}
var index = 0;
foreach (var value in values.OrderBy(static v => v, StringComparer.OrdinalIgnoreCase))
{
if (index++ > 0)
{
builder.Append(',');
}
builder.Append(value);
}
}
}

View File

@@ -522,6 +522,71 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task AdvisoryChunksEndpoint_EmitsRequestAndCacheMetrics()
{
await SeedObservationDocumentsAsync(BuildSampleObservationDocuments());
using var client = _factory.CreateClient();
var metrics = await CaptureMetricsAsync(
AdvisoryAiMetrics.MeterName,
new[] { "advisory_ai_chunk_requests_total", "advisory_ai_chunk_cache_hits_total" },
async () =>
{
const string url = "/advisories/CVE-2025-0001/chunks?tenant=tenant-a";
var first = await client.GetAsync(url);
first.EnsureSuccessStatusCode();
var second = await client.GetAsync(url);
second.EnsureSuccessStatusCode();
});
Assert.True(metrics.TryGetValue("advisory_ai_chunk_requests_total", out var requests));
Assert.NotNull(requests);
Assert.Equal(2, requests!.Count);
Assert.Contains(requests!, measurement =>
string.Equals(GetTagValue(measurement, "cache"), "miss", StringComparison.Ordinal));
Assert.Contains(requests!, measurement =>
string.Equals(GetTagValue(measurement, "cache"), "hit", StringComparison.Ordinal));
Assert.True(metrics.TryGetValue("advisory_ai_chunk_cache_hits_total", out var cacheHitMeasurements));
var cacheHit = Assert.Single(cacheHitMeasurements!);
Assert.Equal(1, cacheHit.Value);
Assert.Equal("hit", GetTagValue(cacheHit, "result"));
}
[Fact]
public async Task AdvisoryChunksEndpoint_EmitsGuardrailMetrics()
{
var raw = BsonDocument.Parse("{\"details\":\"tiny\"}");
var document = CreateChunkObservationDocument(
"tenant-a:chunk:1",
"tenant-a",
new DateTime(2025, 2, 1, 0, 0, 0, DateTimeKind.Utc),
"CVE-2025-GUARD",
raw);
await SeedObservationDocumentsAsync(new[] { document });
using var client = _factory.CreateClient();
var guardrailMetrics = await CaptureMetricsAsync(
AdvisoryAiMetrics.MeterName,
"advisory_ai_guardrail_blocks_total",
async () =>
{
var response = await client.GetAsync("/advisories/CVE-2025-GUARD/chunks?tenant=tenant-a");
response.EnsureSuccessStatusCode();
});
var measurement = Assert.Single(guardrailMetrics);
Assert.True(measurement.Value >= 1);
Assert.Equal("below_minimum_length", GetTagValue(measurement, "reason"));
}
[Fact]
public async Task AdvisoryIngestEndpoint_EmitsMetricsWithExpectedTags()
{
@@ -2069,13 +2134,28 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
private static async Task<IReadOnlyList<MetricMeasurement>> CaptureMetricsAsync(string meterName, string instrumentName, Func<Task> action)
{
var measurements = new List<MetricMeasurement>();
var map = await CaptureMetricsAsync(meterName, new[] { instrumentName }, action).ConfigureAwait(false);
return map.TryGetValue(instrumentName, out var measurements)
? measurements
: Array.Empty<MetricMeasurement>();
}
private static async Task<Dictionary<string, IReadOnlyList<MetricMeasurement>>> CaptureMetricsAsync(
string meterName,
IReadOnlyCollection<string> instrumentNames,
Func<Task> action)
{
var measurementMap = instrumentNames.ToDictionary(
name => name,
_ => new List<MetricMeasurement>(),
StringComparer.Ordinal);
var instrumentSet = new HashSet<string>(instrumentNames, StringComparer.Ordinal);
var listener = new MeterListener();
listener.InstrumentPublished += (instrument, currentListener) =>
{
if (string.Equals(instrument.Meter.Name, meterName, StringComparison.Ordinal) &&
string.Equals(instrument.Name, instrumentName, StringComparison.Ordinal))
instrumentSet.Contains(instrument.Name))
{
currentListener.EnableMeasurementEvents(instrument);
}
@@ -2083,13 +2163,18 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
{
if (!measurementMap.TryGetValue(instrument.Name, out var list))
{
return;
}
var tagDictionary = new Dictionary<string, object?>(StringComparer.Ordinal);
foreach (var tag in tags)
{
tagDictionary[tag.Key] = tag.Value;
}
measurements.Add(new MetricMeasurement(instrument.Name, measurement, tagDictionary));
list.Add(new MetricMeasurement(instrument.Name, measurement, tagDictionary));
});
listener.Start();
@@ -2102,7 +2187,9 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
listener.Dispose();
}
return measurements;
return measurementMap.ToDictionary(
kvp => kvp.Key,
kvp => (IReadOnlyList<MetricMeasurement>)kvp.Value);
}
private static string? GetTagValue(MetricMeasurement measurement, string tag)