From c55a75cb4d91f11e5a54ad6126b004e2987c97f1 Mon Sep 17 00:00:00 2001 From: Rafal Mielowski Date: Thu, 26 Feb 2026 14:50:44 +0100 Subject: [PATCH] Add emulator handler and tests for EmitMetric - Implement EmitMetricHandler with MetricStore collection - Add MetricStore and EmittedMetric record for asserting emitted metrics - Register MetricStore in GatewayContext and TestDocumentExtensions - Add EmitMetricTests with 6 test cases - Metrics collected with name, value, namespace, and dimensions - Default value is 1 when not specified - Callback overrides bypass metric collection - Sections: Inbound, Outbound, OnError Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/EmulatorPolicyChecklist.md | 2 +- .../Document/TestDocumentExtensions.cs | 3 + src/Testing/Emulator/Data/EmittedMetric.cs | 12 ++ src/Testing/Emulator/Data/MetricStore.cs | 13 ++ .../Emulator/Policies/EmitMetricHandler.cs | 9 +- src/Testing/GatewayContext.cs | 1 + .../Emulator/Policies/EmitMetricTests.cs | 145 ++++++++++++++++++ 7 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 src/Testing/Emulator/Data/EmittedMetric.cs create mode 100644 src/Testing/Emulator/Data/MetricStore.cs create mode 100644 test/Test.Testing/Emulator/Policies/EmitMetricTests.cs diff --git a/docs/EmulatorPolicyChecklist.md b/docs/EmulatorPolicyChecklist.md index f9faf9cd..6f3807d9 100644 --- a/docs/EmulatorPolicyChecklist.md +++ b/docs/EmulatorPolicyChecklist.md @@ -49,7 +49,7 @@ Track progress of emulator policy handler implementation. Each policy needs a ha | ⬜ | CacheLookup | CacheLookupHandler.cs | Store interaction | `emulator/cache-lookup` | | ⬜ | CacheStore | CacheStoreHandler.cs | Store interaction | `emulator/cache-store` | | ⬜ | Cors | CorsHandler.cs | Context mutation | `emulator/cors` | -| ⬜ | EmitMetric | EmitMetricHandler.cs | No-op + callbacks | `emulator/emit-metric` | +| ✅ | EmitMetric | EmitMetricHandler.cs | No-op + callbacks | `emulator/emit-metric` | | ⬜ | ForwardRequest | ForwardRequestHandler.cs | External service mock | `emulator/forward-request` | | ⬜ | JsonP | JsonPHandler.cs | Context mutation | `emulator/jsonp` | | ⬜ | JsonToXml | JsonToXmlHandle.cs | No-op + callbacks | `emulator/json-to-xml` | diff --git a/src/Testing/Document/TestDocumentExtensions.cs b/src/Testing/Document/TestDocumentExtensions.cs index 6a91eb81..be9cd7e8 100644 --- a/src/Testing/Document/TestDocumentExtensions.cs +++ b/src/Testing/Document/TestDocumentExtensions.cs @@ -31,4 +31,7 @@ public static ResponseExampleStore SetupResponseExampleStore(this TestDocument d public static LoggerStore SetupLoggerStore(this TestDocument document) => document.Context.LoggerStore; + + public static MetricStore SetupMetricStore(this TestDocument document) => + document.Context.MetricStore; } \ No newline at end of file diff --git a/src/Testing/Emulator/Data/EmittedMetric.cs b/src/Testing/Emulator/Data/EmittedMetric.cs new file mode 100644 index 00000000..895c20aa --- /dev/null +++ b/src/Testing/Emulator/Data/EmittedMetric.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring; + +namespace Microsoft.Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Data; + +public record EmittedMetric( + string Name, + double Value, + string? Namespace, + MetricDimensionConfig[] Dimensions); diff --git a/src/Testing/Emulator/Data/MetricStore.cs b/src/Testing/Emulator/Data/MetricStore.cs new file mode 100644 index 00000000..61dc7ee1 --- /dev/null +++ b/src/Testing/Emulator/Data/MetricStore.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; + +namespace Microsoft.Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Data; + +public class MetricStore +{ + internal readonly IList MetricsInternal = new List(); + + public ImmutableArray Metrics => MetricsInternal.ToImmutableArray(); +} diff --git a/src/Testing/Emulator/Policies/EmitMetricHandler.cs b/src/Testing/Emulator/Policies/EmitMetricHandler.cs index 1e0679e9..5a40adb7 100644 --- a/src/Testing/Emulator/Policies/EmitMetricHandler.cs +++ b/src/Testing/Emulator/Policies/EmitMetricHandler.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring; +using Microsoft.Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Data; namespace Microsoft.Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Policies; @@ -16,6 +17,12 @@ internal class EmitMetricHandler : PolicyHandler protected override void Handle(GatewayContext context, EmitMetricConfig config) { - throw new NotImplementedException(); + var metric = new EmittedMetric( + config.Name, + config.Value ?? 1, + config.Namespace, + config.Dimensions); + + context.MetricStore.MetricsInternal.Add(metric); } } \ No newline at end of file diff --git a/src/Testing/GatewayContext.cs b/src/Testing/GatewayContext.cs index 724b10b6..a7138797 100644 --- a/src/Testing/GatewayContext.cs +++ b/src/Testing/GatewayContext.cs @@ -18,6 +18,7 @@ public class GatewayContext : MockExpressionContext internal readonly CacheStore CacheStore = new(); internal readonly ResponseExampleStore ResponseExampleStore = new(); internal readonly LoggerStore LoggerStore = new(); + internal readonly MetricStore MetricStore = new(); public GatewayContext() { diff --git a/test/Test.Testing/Emulator/Policies/EmitMetricTests.cs b/test/Test.Testing/Emulator/Policies/EmitMetricTests.cs new file mode 100644 index 00000000..e847ee67 --- /dev/null +++ b/test/Test.Testing/Emulator/Policies/EmitMetricTests.cs @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Azure.ApiManagement.PolicyToolkit.Authoring; +using Microsoft.Azure.ApiManagement.PolicyToolkit.Testing; +using Microsoft.Azure.ApiManagement.PolicyToolkit.Testing.Document; + +namespace Test.Emulator.Emulator.Policies; + +[TestClass] +public class EmitMetricTests +{ + class SimpleEmitMetric : IDocument + { + public void Inbound(IInboundContext context) + { + context.EmitMetric(new EmitMetricConfig + { + Name = "my-metric", + Value = 1, + Dimensions = [new MetricDimensionConfig { Name = "region", Value = "west" }] + }); + } + + public void Outbound(IOutboundContext context) + { + context.EmitMetric(new EmitMetricConfig + { + Name = "outbound-metric", + Value = 2.5, + Dimensions = [new MetricDimensionConfig { Name = "status" }] + }); + } + + public void OnError(IOnErrorContext context) + { + context.EmitMetric(new EmitMetricConfig + { + Name = "error-metric", + Value = 1, + Dimensions = [new MetricDimensionConfig { Name = "error-type" }] + }); + } + } + + class MultiEmitMetric : IDocument + { + public void Inbound(IInboundContext context) + { + context.EmitMetric(new EmitMetricConfig + { + Name = "metric-a", + Dimensions = [new MetricDimensionConfig { Name = "dim" }] + }); + context.EmitMetric(new EmitMetricConfig + { + Name = "metric-b", + Value = 5, + Dimensions = [new MetricDimensionConfig { Name = "dim" }] + }); + } + } + + [TestMethod] + public void EmitMetric_Inbound() + { + var test = new SimpleEmitMetric().AsTestDocument(); + + test.RunInbound(); + + var metrics = test.SetupMetricStore().Metrics; + metrics.Should().HaveCount(1); + metrics[0].Name.Should().Be("my-metric"); + metrics[0].Value.Should().Be(1); + metrics[0].Dimensions.Should().HaveCount(1); + metrics[0].Dimensions[0].Name.Should().Be("region"); + } + + [TestMethod] + public void EmitMetric_Outbound() + { + var test = new SimpleEmitMetric().AsTestDocument(); + + test.RunOutbound(); + + var metrics = test.SetupMetricStore().Metrics; + metrics.Should().HaveCount(1); + metrics[0].Name.Should().Be("outbound-metric"); + metrics[0].Value.Should().Be(2.5); + } + + [TestMethod] + public void EmitMetric_OnError() + { + var test = new SimpleEmitMetric().AsTestDocument(); + + test.RunOnError(); + + var metrics = test.SetupMetricStore().Metrics; + metrics.Should().HaveCount(1); + metrics[0].Name.Should().Be("error-metric"); + } + + [TestMethod] + public void EmitMetric_DefaultValueIsOne() + { + var test = new MultiEmitMetric().AsTestDocument(); + + test.RunInbound(); + + var metrics = test.SetupMetricStore().Metrics; + metrics[0].Value.Should().Be(1); + } + + [TestMethod] + public void EmitMetric_MultipleMetricsCollected() + { + var test = new MultiEmitMetric().AsTestDocument(); + + test.RunInbound(); + + var metrics = test.SetupMetricStore().Metrics; + metrics.Should().HaveCount(2); + metrics[0].Name.Should().Be("metric-a"); + metrics[1].Name.Should().Be("metric-b"); + metrics[1].Value.Should().Be(5); + } + + [TestMethod] + public void EmitMetric_Callback() + { + var test = new SimpleEmitMetric().AsTestDocument(); + var callbackExecuted = false; + test.SetupInbound().EmitMetric().WithCallback((_, config) => + { + callbackExecuted = true; + config.Name.Should().Be("my-metric"); + }); + + test.RunInbound(); + + callbackExecuted.Should().BeTrue(); + test.SetupMetricStore().Metrics.Should().BeEmpty(); + } +}