diff --git a/Directory.Packages.props b/Directory.Packages.props index 2e8c89d..fa74aee 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,9 +1,9 @@ - - - + + + @@ -17,7 +17,7 @@ - + diff --git a/README.md b/README.md index e8cc652..734ffa6 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ For API Versioning support: dotnet add package ServiceLevelIndicators.Asp.ApiVersioning ``` -## Usage for Web API MVC +## Usage for ASP.NET Core MVC 1. Register SLI with open telemetry by calling `AddServiceLevelIndicatorInstrumentation`. @@ -142,7 +142,7 @@ dotnet add package ServiceLevelIndicators.Asp.ApiVersioning app.UseServiceLevelIndicator(); ``` -## Usage for Minimal API +## Usage for Minimal APIs 1. Register SLI with open telemetry by calling `AddServiceLevelIndicatorInstrumentation`. @@ -186,7 +186,7 @@ dotnet add package ServiceLevelIndicators.Asp.ApiVersioning .AddServiceLevelIndicator(); ``` -### Usage for background jobs +## Usage for Background Jobs You can measure a block of code by wrapping it in a `using` clause of `MeasuredOperation`. @@ -201,14 +201,16 @@ async Task MeasureCodeBlock(ServiceLevelIndicator serviceLevelIndicator) } ``` -### Customizations +## Operational Guidance -### Cardinality guidance +### Cardinality Guidance Metric dimensions should stay bounded. `CustomerResourceId` and values captured with `[Measure]` are useful when they represent a stable tenant, customer group, plan, environment, or region, but they become expensive if you feed them raw per-user or highly variable values. Prefer values with a controlled set of outcomes. Avoid using email addresses, request IDs, timestamps, or unconstrained free text unless your metrics backend is explicitly designed for high-cardinality telemetry. +## ASP.NET Core Customizations + Once the Prerequisites are done, all controllers will emit SLI information. The default operation name is in the format <HTTP Method> <Controller>/<Action>. eg GET WeatherForecast/Action1 @@ -316,25 +318,16 @@ eg GET WeatherForecast/Action1 - To prevent automatically emitting SLI information on all controllers, set the option, ``` csharp - ServiceLevelIndicatorOptions.AutomaticallyEmitted = false; + builder.Services.AddServiceLevelIndicator(options => + { + options.AutomaticallyEmitted = false; + }) + .AddMvc(); ``` In this case, add the attribute `[ServiceLevelIndicator]` on the controllers that should emit SLI. -- To measure a process, run it within a `using StartMeasuring` block. - - Example: - - ```csharp - public void StoreItem(MyDomainEvent domainEvent) - { - var attribute = new KeyValuePair("Event", domainEvent.GetType().Name); - using var measuredOperation = _serviceLevelIndicator.StartMeasuring("StoreItem", attribute); - DoTheWork(); - } - ``` - -### Sample +## Sample Try out the sample weather forecast Web API. diff --git a/ServiceLevelIndicators/src/README.md b/ServiceLevelIndicators/src/README.md index a6fcf21..70a688b 100644 --- a/ServiceLevelIndicators/src/README.md +++ b/ServiceLevelIndicators/src/README.md @@ -43,22 +43,26 @@ builder.Services.AddOpenTelemetry() metrics.AddOtlpExporter(); }); -builder.Services.AddServiceLevelIndicator(options => +builder.Services.Configure(options => { options.Meter = sliMeter; options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "westus3"); options.CustomerResourceId = "my-customer"; }); + +builder.Services.AddSingleton(); ``` ### 2. Configure options ```csharp -builder.Services.AddServiceLevelIndicator(options => +builder.Services.Configure(options => { options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "westus3"); options.CustomerResourceId = "my-customer"; }); + +builder.Services.AddSingleton(); ``` ### 3. Measure operations diff --git a/docs/usage-reference.md b/docs/usage-reference.md index b32f9e2..72c0896 100644 --- a/docs/usage-reference.md +++ b/docs/usage-reference.md @@ -48,11 +48,13 @@ builder.Services.AddOpenTelemetry() Register the service: ```csharp -builder.Services.AddServiceLevelIndicator(options => +builder.Services.Configure(options => { options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "westus3"); options.CustomerResourceId = "tenant-a"; }); + +builder.Services.AddSingleton(); ``` Measure work: @@ -89,12 +91,14 @@ builder.Services.AddOpenTelemetry() metrics.AddOtlpExporter(); }); -builder.Services.AddServiceLevelIndicator(options => +builder.Services.Configure(options => { options.Meter = sliMeter; options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "westus3"); options.CustomerResourceId = "tenant-a"; }); + +builder.Services.AddSingleton(); ``` Available registration overloads: diff --git a/sample/MinApi/Program.cs b/sample/MinApi/Program.cs index f58e5de..6a50fe1 100644 --- a/sample/MinApi/Program.cs +++ b/sample/MinApi/Program.cs @@ -1,6 +1,7 @@ using Azure.Core; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; +using Scalar.AspNetCore; using SampleMinimalApiSli; using ServiceLevelIndicators; @@ -13,14 +14,7 @@ "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(options => -{ - - var fileName = typeof(Program).Assembly.GetName().Name + ".xml"; - var filePath = Path.Combine(AppContext.BaseDirectory, fileName); - options.IncludeXmlComments(filePath); -}); +builder.Services.AddOpenApi(); // Build a resource configuration action to set service information. @@ -65,8 +59,8 @@ .AddServiceLevelIndicator("background_work"); app.UseUserRoute(); -app.UseSwagger(); -app.UseSwaggerUI(); +app.MapOpenApi(); +app.MapScalarApiReference(); app.UseHttpsRedirection(); app.UseServiceLevelIndicator(); app.Run(); diff --git a/sample/MinApi/Properties/launchSettings.json b/sample/MinApi/Properties/launchSettings.json index 46e4f27..a940696 100644 --- a/sample/MinApi/Properties/launchSettings.json +++ b/sample/MinApi/Properties/launchSettings.json @@ -3,7 +3,7 @@ "SampleMinimalApiSli": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "swagger", + "launchUrl": "scalar/v1", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/sample/MinApi/SampleMinimalApiSli.csproj b/sample/MinApi/SampleMinimalApiSli.csproj index ad17a2b..8469fa6 100644 --- a/sample/MinApi/SampleMinimalApiSli.csproj +++ b/sample/MinApi/SampleMinimalApiSli.csproj @@ -7,7 +7,8 @@ - + + diff --git a/sample/WebApi/Program.cs b/sample/WebApi/Program.cs index 7eb2fdc..0a0795b 100644 --- a/sample/WebApi/Program.cs +++ b/sample/WebApi/Program.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Options; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; +using Scalar.AspNetCore; using SampleWebApplicationSLI; using ServiceLevelIndicators; @@ -11,15 +12,9 @@ // Add services to the container. builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +// Learn more about configuring the built-in OpenAPI + Scalar setup in ASP.NET Core builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(options => -{ - - var fileName = typeof(Program).Assembly.GetName().Name + ".xml"; - var filePath = Path.Combine(AppContext.BaseDirectory, fileName); - options.IncludeXmlComments(filePath); -}); +builder.Services.AddOpenApi(); builder.Services.AddProblemDetails(); // Build a resource configuration action to set service information. @@ -47,8 +42,8 @@ var app = builder.Build(); -app.UseSwagger(); -app.UseSwaggerUI(); +app.MapOpenApi(); +app.MapScalarApiReference(); app.UseHttpsRedirection(); app.UseServiceLevelIndicator(); app.UseAuthorization(); diff --git a/sample/WebApi/Properties/launchSettings.json b/sample/WebApi/Properties/launchSettings.json index 51932bc..0649eac 100644 --- a/sample/WebApi/Properties/launchSettings.json +++ b/sample/WebApi/Properties/launchSettings.json @@ -3,7 +3,7 @@ "SampleWebApplicationSLI": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "swagger", + "launchUrl": "scalar/v1", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/sample/WebApi/SampleWebApplicationSLI.csproj b/sample/WebApi/SampleWebApplicationSLI.csproj index ad17a2b..8469fa6 100644 --- a/sample/WebApi/SampleWebApplicationSLI.csproj +++ b/sample/WebApi/SampleWebApplicationSLI.csproj @@ -7,7 +7,8 @@ - + + diff --git a/sample/WebApiVersioned/AddApiVersionMetadata.cs b/sample/WebApiVersioned/AddApiVersionMetadata.cs deleted file mode 100644 index f8e7a7d..0000000 --- a/sample/WebApiVersioned/AddApiVersionMetadata.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace SampleVersionedWebApplicationSLI; - -using System.Globalization; -using System.Text.Json; -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; - -/// -/// Represents the OpenAPI/Swashbuckle operation filter used to document information provided, but not used. -/// -/// This is only required due to bugs in the . -/// Once they are fixed and published, this class can be removed. -public class AddApiVersionMetadata : IOperationFilter -{ - /// - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - var apiDescription = context.ApiDescription; - - operation.Deprecated |= apiDescription.IsDeprecated; - - // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1752#issue-663991077 - foreach (var responseType in context.ApiDescription.SupportedResponseTypes) - { - // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/b7cf75e7905050305b115dd96640ddd6e74c7ac9/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs#L383-L387 - var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString(CultureInfo.InvariantCulture); - var response = operation.Responses[responseKey]; - - foreach (var contentType in response.Content.Keys) - { - if (!responseType.ApiResponseFormats.Any(x => x.MediaType == contentType)) - { - response.Content.Remove(contentType); - } - } - } - - if (operation.Parameters == null) - { - return; - } - - // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412 - // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413 - foreach (var parameter in operation.Parameters) - { - var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name); - - if (parameter.Description == null) - { - parameter.Description = description.ModelMetadata?.Description; - } - - if (parameter.Schema.Default == null && - description.DefaultValue != null && - description.DefaultValue is not DBNull && - description.ModelMetadata is ModelMetadata modelMetadata) - { - // REF: https://github.com/Microsoft/aspnet-api-versioning/issues/429#issuecomment-605402330 - var json = JsonSerializer.Serialize(description.DefaultValue, modelMetadata.ModelType); - parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json); - } - - parameter.Required |= description.IsRequired; - } - } -} \ No newline at end of file diff --git a/sample/WebApiVersioned/ConfigureSwaggerDefaultOptions.cs b/sample/WebApiVersioned/ConfigureSwaggerDefaultOptions.cs deleted file mode 100644 index 7a3279f..0000000 --- a/sample/WebApiVersioned/ConfigureSwaggerDefaultOptions.cs +++ /dev/null @@ -1,89 +0,0 @@ -namespace SampleVersionedWebApplicationSLI; - -using System.Text; -using Asp.Versioning; -using Asp.Versioning.ApiExplorer; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; - -/// -/// Configures the Swagger generation options. -/// -/// This allows API versioning to define a Swagger document per API version after the -/// service has been resolved from the service container. -public class ConfigureSwaggerDefaultOptions : IConfigureOptions -{ - private readonly IApiVersionDescriptionProvider provider; - - /// - /// Initializes a new instance of the class. - /// - /// The provider used to generate Swagger documents. - public ConfigureSwaggerDefaultOptions(IApiVersionDescriptionProvider provider) => this.provider = provider; - - /// - public void Configure(SwaggerGenOptions options) - { - // add a swagger document for each discovered API version - // note: you might choose to skip or document deprecated API versions differently - foreach (var description in provider.ApiVersionDescriptions) - { - options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description)); - } - } - - private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description) - { - var text = new StringBuilder("An example application with OpenAPI, Swashbuckle, and API versioning."); - var info = new OpenApiInfo() - { - Title = "Best Weather forecast API", - Version = description.ApiVersion.ToString(), - Contact = new OpenApiContact() { Name = "Xavier John", Email = "xavier@somewhere.com" }, - License = new OpenApiLicense() { Name = "MIT", Url = new Uri("https://opensource.org/licenses/MIT") } - }; - - if (description.IsDeprecated) - { - text.Append(" This API version has been deprecated."); - } - - if (description.SunsetPolicy is SunsetPolicy policy) - { - if (policy.Date is DateTimeOffset when) - { - text.Append(" The API will be sunset on ") - .Append(when.Date.ToShortDateString()) - .Append('.'); - } - - if (policy.HasLinks) - { - text.AppendLine(); - - for (var i = 0; i < policy.Links.Count; i++) - { - var link = policy.Links[i]; - - if (link.Type == "text/html") - { - text.AppendLine(); - - if (link.Title.HasValue) - { - text.Append(link.Title.Value).Append(": "); - } - - text.Append(link.LinkTarget.OriginalString); - } - } - } - } - - info.Description = text.ToString(); - - return info; - } -} \ No newline at end of file diff --git a/sample/WebApiVersioned/Program.cs b/sample/WebApiVersioned/Program.cs index 742b310..35a6dfa 100644 --- a/sample/WebApiVersioned/Program.cs +++ b/sample/WebApiVersioned/Program.cs @@ -1,31 +1,15 @@ using Azure.Core; -using Microsoft.Extensions.Options; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; -using SampleVersionedWebApplicationSLI; +using Scalar.AspNetCore; using ServiceLevelIndicators; -using Swashbuckle.AspNetCore.SwaggerGen; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddTransient, ConfigureSwaggerDefaultOptions>(); -builder.Services.AddSwaggerGen( - options => - { - // add a custom operation filter which sets default values - options.OperationFilter(); - - var fileName = typeof(Program).Assembly.GetName().Name + ".xml"; - var filePath = Path.Combine(AppContext.BaseDirectory, fileName); - - // integrate XML comments - options.IncludeXmlComments(filePath); - }); +builder.Services.AddOpenApi(); builder.Services.AddApiVersioning() .AddMvc() .AddApiExplorer(); @@ -49,21 +33,10 @@ var app = builder.Build(); -app.UseSwagger(); -app.UseSwaggerUI( - options => - { - options.RoutePrefix = string.Empty; // make home page the swagger UI - var descriptions = app.DescribeApiVersions(); - - // build a swagger endpoint for each discovered API version - foreach (var description in descriptions) - { - var url = $"/swagger/{description.GroupName}/swagger.json"; - var name = description.GroupName.ToUpperInvariant(); - options.SwaggerEndpoint(url, name); - } - }); +// TODO: Use .AddOpenApi() from Asp.Versioning.OpenApi with WithDocumentPerVersion() +// and AddScalarTransformers() once a stable release is available. +app.MapOpenApi(); +app.MapScalarApiReference(); // Random delay. Random rnd = new Random(); diff --git a/sample/WebApiVersioned/Properties/launchSettings.json b/sample/WebApiVersioned/Properties/launchSettings.json index 5a64df5..fb1873e 100644 --- a/sample/WebApiVersioned/Properties/launchSettings.json +++ b/sample/WebApiVersioned/Properties/launchSettings.json @@ -3,6 +3,7 @@ "SampleVersionedWebApplicationSLI": { "commandName": "Project", "launchBrowser": true, + "launchUrl": "scalar/v1", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/sample/WebApiVersioned/SampleVersionedWebApplicationSLI.csproj b/sample/WebApiVersioned/SampleVersionedWebApplicationSLI.csproj index 77707f7..b276694 100644 --- a/sample/WebApiVersioned/SampleVersionedWebApplicationSLI.csproj +++ b/sample/WebApiVersioned/SampleVersionedWebApplicationSLI.csproj @@ -9,7 +9,8 @@ - + +