From 96da2604d28c0107a47a2d0f61dab16319f158e4 Mon Sep 17 00:00:00 2001 From: Delphin Habierre Date: Wed, 31 May 2023 22:57:05 +0200 Subject: [PATCH] Add api-version support --- .../CHANGELOG.md | 3 + .../Implementation/HttpInListener.cs | 34 +++++++++++ test/Directory.Packages.props | 4 +- .../BasicTests.cs | 51 ++++++++++++++++ .../Controllers/ChildActivityController.cs | 2 + .../Controllers/ErrorController.cs | 2 + .../Controllers/V1/ApiVersioningController.cs | 41 +++++++++++++ .../Controllers/V2/ApiVersioningController.cs | 41 +++++++++++++ .../Controllers/ValuesController.cs | 2 + test/TestApp.AspNetCore/Program.cs | 34 ++++++++++- .../Swagger/ConfigureSwaggerOptions.cs | 61 +++++++++++++++++++ .../TestApp.AspNetCore.csproj | 2 + 12 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 test/TestApp.AspNetCore/Controllers/V1/ApiVersioningController.cs create mode 100644 test/TestApp.AspNetCore/Controllers/V2/ApiVersioningController.cs create mode 100644 test/TestApp.AspNetCore/Swagger/ConfigureSwaggerOptions.cs diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md index c52cb23f52b..26484e29cbc 100644 --- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +* Added [api-versioning](https://github.com/dotnet/aspnet-api-versioning/wiki) support + [#2967](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2967), [4525](https://github.com/open-telemetry/opentelemetry-dotnet/issues/4525) + ## 1.5.0-rc.1 Released 2023-May-25 diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs index 537ff5ea42e..4bc8cfe9b75 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs @@ -333,6 +333,9 @@ public void OnMvcBeforeAction(Activity activity, object payload) if (!string.IsNullOrEmpty(template)) { // override the span name that was previously set to the path part of URL. + + template = GetApiVersioningEndpoint(template, activity); + activity.DisplayName = template; activity.SetTag(SemanticConventions.AttributeHttpRoute, template); } @@ -372,6 +375,37 @@ public void OnException(Activity activity, object payload) } } + /// + /// Returns the template with the real '{version:apiVersion}' version. + /// Example: + /// * template = 'api/v{version:apiVersion}/ApiVersioning/{id}' + /// * http.target = '/api/v1/ApiVersioning/{id}' + /// The result will be: 'api/v1/ApiVersioning/{id}'. + /// + /// The route template. + /// The activity instance. + /// The template with the real '{version:apiVersion}' version. + private static string GetApiVersioningEndpoint(string template, Activity activity) + { + var endpoint = template; + + const string apiVersionSegment = "{version:apiVersion}"; + + if (template.Contains(apiVersionSegment)) + { + var templateParts = template.Split('/'); + var httpTargetParts = (activity.GetTagValue(SemanticConventions.AttributeHttpTarget) as string).Split('/'); + + var index = templateParts.TakeWhile(x => !x.Contains(apiVersionSegment)).Count(); + + templateParts[index] = httpTargetParts[index + 1]; // replace '{version:apiVersion}' segment with the real version without changing other tokens + + endpoint = string.Join("/", templateParts); + } + + return endpoint; + } + private static string GetUri(HttpRequest request) { // this follows the suggestions from https://github.com/dotnet/aspnetcore/issues/28906 diff --git a/test/Directory.Packages.props b/test/Directory.Packages.props index 9a73f74f978..97455b44058 100644 --- a/test/Directory.Packages.props +++ b/test/Directory.Packages.props @@ -4,5 +4,7 @@ + + - + \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs index d8b3737ead0..7dc71b52bd2 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs @@ -96,6 +96,57 @@ void ConfigureTestServices(IServiceCollection services) ValidateAspNetCoreActivity(activity, "/api/values"); } + [Theory] + [InlineData("v1")] + [InlineData("v2")] + public async Task StatusWithApiVersionIsUnsetOn200Response(string apiVersion) + { + var exportedItems = new List(); + var expectedTraceId = ActivityTraceId.CreateRandom(); + var expectedSpanId = ActivitySpanId.CreateRandom(); + + // Arrange + using (var testFactory = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(services => + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); + }); + + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + })) + { + using var client = testFactory.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Get, $"/api/{apiVersion}/ApiVersioning/42"); + request.Headers.Add("traceparent", $"00-{expectedTraceId}-{expectedSpanId}-01"); + + // Act + var response = await client.SendAsync(request).ConfigureAwait(false); + + // Assert + response.EnsureSuccessStatusCode(); // Status Code 200-299 + + WaitForActivityExport(exportedItems, 1); + } + + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", activity.OperationName); + Assert.Equal($"api/{apiVersion}/ApiVersioning/{{id}}", activity.DisplayName); + + Assert.Equal(expectedTraceId, activity.Context.TraceId); + Assert.Equal(expectedSpanId, activity.ParentSpanId); + + Assert.Equal($"api/{apiVersion}/ApiVersioning/{{id}}", activity.GetTagValue(SemanticConventions.AttributeHttpRoute)); + + ValidateAspNetCoreActivity(activity, $"/api/{apiVersion}/ApiVersioning/42"); + } + [Theory] [InlineData(true)] [InlineData(false)] diff --git a/test/TestApp.AspNetCore/Controllers/ChildActivityController.cs b/test/TestApp.AspNetCore/Controllers/ChildActivityController.cs index e7c8c1a1959..2fb0e036794 100644 --- a/test/TestApp.AspNetCore/Controllers/ChildActivityController.cs +++ b/test/TestApp.AspNetCore/Controllers/ChildActivityController.cs @@ -20,8 +20,10 @@ namespace TestApp.AspNetCore.Controllers { + [ApiController] public class ChildActivityController : Controller { + [ApiVersionNeutral] [HttpGet] [Route("api/GetChildActivityTraceContext")] public Dictionary GetChildActivityTraceContext() diff --git a/test/TestApp.AspNetCore/Controllers/ErrorController.cs b/test/TestApp.AspNetCore/Controllers/ErrorController.cs index 174888fec05..dd090c40860 100644 --- a/test/TestApp.AspNetCore/Controllers/ErrorController.cs +++ b/test/TestApp.AspNetCore/Controllers/ErrorController.cs @@ -17,6 +17,8 @@ namespace TestApp.AspNetCore.Controllers { + [ApiController] + [ApiVersionNeutral] [Route("api/[controller]")] public class ErrorController : Controller { diff --git a/test/TestApp.AspNetCore/Controllers/V1/ApiVersioningController.cs b/test/TestApp.AspNetCore/Controllers/V1/ApiVersioningController.cs new file mode 100644 index 00000000000..7c37515f698 --- /dev/null +++ b/test/TestApp.AspNetCore/Controllers/V1/ApiVersioningController.cs @@ -0,0 +1,41 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +using Microsoft.AspNetCore.Mvc; + +namespace TestApp.AspNetCore.Controllers.V1 +{ + [ApiController] + [Route("api/v{version:apiVersion}/[controller]")] + [ApiVersion("1.0")] + public class ApiVersioningController : Controller + { + // GET api/v1/apiVersion + [HttpGet] + [MapToApiVersion("1.0")] + public string Get() + { + return "version 1"; + } + + // GET api/v1/apiVersion/42 + [HttpGet("{id}")] + [MapToApiVersion("1.0")] + public string Get(int id) + { + return $"version 1 (id = {id})"; + } + } +} diff --git a/test/TestApp.AspNetCore/Controllers/V2/ApiVersioningController.cs b/test/TestApp.AspNetCore/Controllers/V2/ApiVersioningController.cs new file mode 100644 index 00000000000..a6a2ddb9503 --- /dev/null +++ b/test/TestApp.AspNetCore/Controllers/V2/ApiVersioningController.cs @@ -0,0 +1,41 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +using Microsoft.AspNetCore.Mvc; + +namespace TestApp.AspNetCore.Controllers.V2 +{ + [ApiController] + [Route("api/v{version:apiVersion}/[controller]")] + [ApiVersion("2.0")] + public class ApiVersioningController : Controller + { + // GET api/v2/apiVersion + [HttpGet] + [MapToApiVersion("2.0")] + public string Get() + { + return "version 2"; + } + + // GET api/v2/apiVersion/42 + [HttpGet("{id}")] + [MapToApiVersion("2.0")] + public string Get(int id) + { + return $"version 2 (id = {id})"; + } + } +} diff --git a/test/TestApp.AspNetCore/Controllers/ValuesController.cs b/test/TestApp.AspNetCore/Controllers/ValuesController.cs index 12154514eab..dd1160ee23d 100644 --- a/test/TestApp.AspNetCore/Controllers/ValuesController.cs +++ b/test/TestApp.AspNetCore/Controllers/ValuesController.cs @@ -17,6 +17,8 @@ namespace TestApp.AspNetCore.Controllers { + [ApiController] + [ApiVersionNeutral] [Route("api/[controller]")] public class ValuesController : Controller { diff --git a/test/TestApp.AspNetCore/Program.cs b/test/TestApp.AspNetCore/Program.cs index b68fcf238fa..edc52f38b00 100644 --- a/test/TestApp.AspNetCore/Program.cs +++ b/test/TestApp.AspNetCore/Program.cs @@ -14,7 +14,11 @@ // limitations under the License. // +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Versioning; using TestApp.AspNetCore; +using TestApp.AspNetCore.Swagger; public class Program { @@ -29,6 +33,26 @@ public static void Main(string[] args) // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddApiVersioning(setup => + { + setup.DefaultApiVersion = new ApiVersion(1, 0); + setup.AssumeDefaultVersionWhenUnspecified = true; + setup.ReportApiVersions = true; + + setup.ApiVersionReader = ApiVersionReader.Combine( + new UrlSegmentApiVersionReader(), + new HeaderApiVersionReader("x-api-version"), + new MediaTypeApiVersionReader("x-api-version")); + }); + + builder.Services.ConfigureOptions(); + + builder.Services.AddVersionedApiExplorer(setup => + { + setup.GroupNameFormat = "'v'VVV"; + setup.SubstituteApiVersionInUrl = true; + }); + builder.Services.AddSwaggerGen(); builder.Services.AddMvc(); @@ -46,8 +70,16 @@ public static void Main(string[] args) // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { + var apiVersionDescriptionProvider = app.Services.GetRequiredService(); + app.UseSwagger(); - app.UseSwaggerUI(); + app.UseSwaggerUI(options => + { + foreach (var groupName in apiVersionDescriptionProvider.ApiVersionDescriptions.Select(desc => desc.GroupName)) + { + options.SwaggerEndpoint($"/swagger/{groupName}/swagger.json", groupName.ToUpperInvariant()); + } + }); } app.UseHttpsRedirection(); diff --git a/test/TestApp.AspNetCore/Swagger/ConfigureSwaggerOptions.cs b/test/TestApp.AspNetCore/Swagger/ConfigureSwaggerOptions.cs new file mode 100644 index 00000000000..bf923ce64f5 --- /dev/null +++ b/test/TestApp.AspNetCore/Swagger/ConfigureSwaggerOptions.cs @@ -0,0 +1,61 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace TestApp.AspNetCore.Swagger +{ + public sealed class ConfigureSwaggerOptions : IConfigureNamedOptions + { + private readonly IApiVersionDescriptionProvider provider; + + public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) + { + this.provider = provider ?? throw new ArgumentNullException(nameof(provider)); + } + + public void Configure(string? name, SwaggerGenOptions options) + { + this.Configure(options); + } + + public void Configure(SwaggerGenOptions options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + // Add swagger document for every API version discovered + foreach (var description in this.provider.ApiVersionDescriptions) + { + options.SwaggerDoc(description.GroupName, CreateApiVersion(description)); + } + } + + private static OpenApiInfo CreateApiVersion(ApiVersionDescription description) + { + return new OpenApiInfo + { + Title = "TestApp.AspNetCore", + Version = description.ApiVersion.ToString(), + }; + } + } +} diff --git a/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj b/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj index 241bacbbd47..93496adaed9 100644 --- a/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj +++ b/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj @@ -5,6 +5,8 @@ + +