Skip to content

Commit

Permalink
Add api-version support
Browse files Browse the repository at this point in the history
  • Loading branch information
dhabierre committed May 31, 2023
1 parent 0b81631 commit 96da260
Show file tree
Hide file tree
Showing 12 changed files with 275 additions and 2 deletions.
3 changes: 3 additions & 0 deletions src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -372,6 +375,37 @@ public void OnException(Activity activity, object payload)
}
}

/// <summary>
/// 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}'.
/// </summary>
/// <param name="template">The route template.</param>
/// <param name="activity">The activity instance.</param>
/// <returns>The template with the real '{version:apiVersion}' version.</returns>
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
Expand Down
4 changes: 3 additions & 1 deletion test/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
<PackageVersion Update="Grpc.Tools" Version="[2.44.0,3.0)" />
<PackageVersion Update="Microsoft.Extensions.Logging" Version="[6.0.0,)" />
<PackageVersion Update="System.Text.Json" Version="6.0.5" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="5.1.0" />
</ItemGroup>
</Project>
</Project>
51 changes: 51 additions & 0 deletions test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Activity>();
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)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@

namespace TestApp.AspNetCore.Controllers
{
[ApiController]
public class ChildActivityController : Controller
{
[ApiVersionNeutral]
[HttpGet]
[Route("api/GetChildActivityTraceContext")]
public Dictionary<string, string> GetChildActivityTraceContext()
Expand Down
2 changes: 2 additions & 0 deletions test/TestApp.AspNetCore/Controllers/ErrorController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

namespace TestApp.AspNetCore.Controllers
{
[ApiController]
[ApiVersionNeutral]
[Route("api/[controller]")]
public class ErrorController : Controller
{
Expand Down
41 changes: 41 additions & 0 deletions test/TestApp.AspNetCore/Controllers/V1/ApiVersioningController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// <copyright file="ValuesController.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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})";
}
}
}
41 changes: 41 additions & 0 deletions test/TestApp.AspNetCore/Controllers/V2/ApiVersioningController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// <copyright file="ValuesController.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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})";
}
}
}
2 changes: 2 additions & 0 deletions test/TestApp.AspNetCore/Controllers/ValuesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

namespace TestApp.AspNetCore.Controllers
{
[ApiController]
[ApiVersionNeutral]
[Route("api/[controller]")]
public class ValuesController : Controller
{
Expand Down
34 changes: 33 additions & 1 deletion test/TestApp.AspNetCore/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
// limitations under the License.
// </copyright>

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Versioning;
using TestApp.AspNetCore;
using TestApp.AspNetCore.Swagger;

public class Program
{
Expand All @@ -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<ConfigureSwaggerOptions>();

builder.Services.AddVersionedApiExplorer(setup =>
{
setup.GroupNameFormat = "'v'VVV";
setup.SubstituteApiVersionInUrl = true;
});

builder.Services.AddSwaggerGen();

builder.Services.AddMvc();
Expand All @@ -46,8 +70,16 @@ public static void Main(string[] args)
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
var apiVersionDescriptionProvider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();

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();
Expand Down
61 changes: 61 additions & 0 deletions test/TestApp.AspNetCore/Swagger/ConfigureSwaggerOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// <copyright file="ConfigureSwaggerOptions.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>

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<SwaggerGenOptions>
{
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(),
};
}
}
}
2 changes: 2 additions & 0 deletions test/TestApp.AspNetCore/TestApp.AspNetCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" />
<PackageReference Include="Swashbuckle.AspNetCore" />
</ItemGroup>

Expand Down

0 comments on commit 96da260

Please sign in to comment.