Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9463781
created custom "per route" sampling logic
dylan-asos Jun 4, 2025
38a1e7e
renamed directory and solution
dylan-asos Jun 4, 2025
a32e647
structure updates
dylan-asos Jun 4, 2025
2cac3f8
updating docs
dylan-asos Jun 4, 2025
2fa5588
removed icon usage
dylan-asos Jun 4, 2025
4e14044
keep paths local to projects
dylan-asos Jun 4, 2025
6e29a6a
ongoing work for head/tail based sampling support
dylan-asos Jul 19, 2025
d7a6873
Initial plan
Copilot Jul 19, 2025
a54bede
Start comprehensive documentation improvements
Copilot Jul 19, 2025
11f8898
Complete comprehensive documentation improvements
Copilot Jul 19, 2025
e9833c1
Merge branch 'feature/sampling' into copilot/fix-23
dylan-asos Jul 19, 2025
1d28a5b
[WIP] Improve documentation (#24)
Copilot Jul 19, 2025
ed29953
fixed up test
dylan-asos Jul 20, 2025
a46a288
Update OpenTelemetrySetupTests.cs
dylan-asos Jul 20, 2025
1ee924b
API improvements
dylan-asos Jul 20, 2025
5b408d0
Merge branch 'feature/sampling' of https://github.com/ASOS/asos-open-…
dylan-asos Jul 20, 2025
a13829b
Update azure-pipelines.yml for Azure Pipelines
dylan-asos Jul 20, 2025
94e5218
Update azure-pipelines.yml for Azure Pipelines
dylan-asos Jul 20, 2025
8f68434
Update azure-pipelines.yml for Azure Pipelines
dylan-asos Jul 20, 2025
ff222fa
Simplify API and added a Log Processor
dylan-asos Jul 21, 2025
8520691
Merge remote-tracking branch 'origin/feature/sampling' into feature/s…
dylan-asos Jul 21, 2025
f403f8e
Allow more processing time on fast test
dylan-asos Jul 21, 2025
0a587e4
refactors, validation and setting tags for itemCounts
dylan-asos Jul 21, 2025
b2db585
tweaks to dependency failure detection
dylan-asos Jul 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="NUnit" Version="3.14.0"/>
<PackageReference Include="NUnit.Analyzers" Version="3.9.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
</ItemGroup>

<ItemGroup>
<Using Include="NUnit.Framework"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Asos.OpenTelemetry.AspNetCore\Asos.OpenTelemetry.AspNetCore.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
using System.Text.RegularExpressions;
using Asos.OpenTelemetry.AspNetCore.Sampling;
using Microsoft.AspNetCore.Http;
using NSubstitute;
using OpenTelemetry.Trace;

namespace Asos.OpenTelemetry.AspNetCore.Tests;

[TestFixture]
public class ConfigurableRouteSamplerTests
{
private IHttpContextAccessor _httpContextAccessor;
private RouteSamplingOptions _options;

[SetUp]
public void Setup()
{
_httpContextAccessor = Substitute.For<IHttpContextAccessor>();
_options = new RouteSamplingOptions
{
DefaultRate = 0.5,
SamplingRules =
[
new SamplingRule
{
RoutePattern = "^/api/test$",
Method = "GET",
Rate = 1.0,
CompiledPattern = new Regex("^/api/test$", RegexOptions.IgnoreCase | RegexOptions.Compiled)
}
]
};
}

[Test]
public void ShouldSample_DefaultSamplingRate_WhenNoMatchingRouteOrMethod()
{
var httpContext = new DefaultHttpContext
{
Request = { Path = "/unknown", Method = "POST" }
};
_httpContextAccessor.HttpContext.Returns(httpContext);

var sampler = new ConfigurableRouteSampler(_options, _httpContextAccessor);
var result = sampler.ShouldSample(default);

Assert.That(result.Decision, Is.EqualTo(SamplingDecision.Drop).Or.EqualTo(SamplingDecision.RecordAndSample));
}

[Test]
public void ShouldSample_SpecificRouteAndMethodMatch()
{
var httpContext = new DefaultHttpContext
{
Request = { Path = "/api/test", Method = "GET" }
};
_httpContextAccessor.HttpContext.Returns(httpContext);

var sampler = new ConfigurableRouteSampler(_options, _httpContextAccessor);
var result = sampler.ShouldSample(default);

Assert.That(result.Decision, Is.EqualTo(SamplingDecision.RecordAndSample));
}

[Test]
public void ShouldSample_BoundarySamplingRates()
{
_options.DefaultRate = 0.0;
var sampler = new ConfigurableRouteSampler(_options, _httpContextAccessor);
var result = sampler.ShouldSample(default);

Assert.That(result.Decision, Is.EqualTo(SamplingDecision.Drop));

_options.DefaultRate = 1.0;
sampler = new ConfigurableRouteSampler(_options, _httpContextAccessor);
result = sampler.ShouldSample(default);

Assert.That(result.Decision, Is.EqualTo(SamplingDecision.RecordAndSample));
}

[Test]
public void ShouldSample_NullHttpContext()
{
_httpContextAccessor.HttpContext.Returns((HttpContext)null!);

var sampler = new ConfigurableRouteSampler(_options, _httpContextAccessor);
var result = sampler.ShouldSample(default);

Assert.That(result.Decision, Is.EqualTo(SamplingDecision.Drop).Or.EqualTo(SamplingDecision.RecordAndSample));
}

[Test]
public void ShouldSample_CaseInsensitiveMethodMatching()
{
var httpContext = new DefaultHttpContext
{
Request = { Path = "/api/test", Method = "get" }
};
_httpContextAccessor.HttpContext.Returns(httpContext);

var sampler = new ConfigurableRouteSampler(_options, _httpContextAccessor);
var result = sampler.ShouldSample(default);

Assert.That(result.Decision, Is.EqualTo(SamplingDecision.RecordAndSample));
}

[Test]
public void ShouldSample_RoutePatternMatching()
{
var httpContext = new DefaultHttpContext
{
Request = { Path = "/api/test", Method = "GET" }
};
_httpContextAccessor.HttpContext.Returns(httpContext);

var sampler = new ConfigurableRouteSampler(_options, _httpContextAccessor);
var result = sampler.ShouldSample(default);

Assert.That(result.Decision, Is.EqualTo(SamplingDecision.RecordAndSample));
}

[Test]
public void ShouldSample_EmptySamplingRules()
{
_options.SamplingRules.Clear();

var httpContext = new DefaultHttpContext
{
Request = { Path = "/api/test", Method = "GET" }
};
_httpContextAccessor.HttpContext.Returns(httpContext);

var sampler = new ConfigurableRouteSampler(_options, _httpContextAccessor);
var result = sampler.ShouldSample(default);

Assert.That(result.Decision, Is.EqualTo(SamplingDecision.Drop).Or.EqualTo(SamplingDecision.RecordAndSample));
}

[Test]
public void ShouldSample_InvalidSamplingRate()
{
_options.DefaultRate = -1.0;

var sampler = new ConfigurableRouteSampler(_options, _httpContextAccessor);
var result = sampler.ShouldSample(default);

Assert.That(result.Decision, Is.EqualTo(SamplingDecision.Drop));
}

[Test]
public void ShouldSample_Concurrency()
{
var sampler = new ConfigurableRouteSampler(_options, _httpContextAccessor);

Parallel.For(0, 100, _ =>
{
var result = sampler.ShouldSample(default);
Assert.That(result.Decision, Is.EqualTo(SamplingDecision.Drop).Or.EqualTo(SamplingDecision.RecordAndSample));
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Text.RegularExpressions;
using Asos.OpenTelemetry.AspNetCore.Sampling;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenTelemetry.Trace;

namespace Asos.OpenTelemetry.AspNetCore.Tests;

public class OpenTelemetrySetupTests
{
[Test]
public void ConfigureOpenTelemetry_RegistersRequiredServices()
{
var builder = WebApplication.CreateBuilder();
builder.Configuration["OpenTelemetry:Sampling:SamplingRules:0:RoutePattern"] = "/api/test";

builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

builder.ConfigureOpenTelemetryCustomSampling(options =>
{
options.SamplingRatio = 0.5f;
options.ConnectionString = "InstrumentationKey=12345-12345-12345-12345";
});

var provider = builder.Services.BuildServiceProvider();

var tracerProvider = provider.GetRequiredService<TracerProvider>();
Assert.That(tracerProvider, Is.Not.Null);

// Assert RouteSamplingOptions are bound correctly
var routeSamplingOptions = provider.GetRequiredService<IOptions<RouteSamplingOptions>>().Value;
Assert.That(routeSamplingOptions.SamplingRules, Has.Exactly(1).Items);
Assert.Multiple(() =>
{
Assert.That(routeSamplingOptions.SamplingRules[0].RoutePattern, Is.EqualTo("/api/test"));
Assert.That(routeSamplingOptions.SamplingRules[0].CompiledPattern, Is.Not.Null);
});
Assert.That(routeSamplingOptions.SamplingRules[0].CompiledPattern, Is.InstanceOf<Regex>());

// Assert that ConfigurableRouteSampler is registered
var sampler = provider.GetService<ConfigurableRouteSampler>();
Assert.That(sampler, Is.Not.Null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageOutputPath>./nupkg</PackageOutputPath>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<Authors>asos</Authors>
<Product>Asos.OpenTelemetry.AspNetCore</Product>
<PackageId>Asos.OpenTelemetry.AspNetCore</PackageId>
<Description>OpenTelemetry functionality and extensions for use in AspNetCore applications</Description>
<TargetFramework>net8.0</TargetFramework>
<PackageIcon>otel_icon.png</PackageIcon>
<PackageIconUrl />
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="." />
<None Include="otel_icon.png" Pack="true" PackagePath="." />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Monitor.OpenTelemetry.AspNetCore" Version="1.2.0" />
</ItemGroup>
</Project>
69 changes: 69 additions & 0 deletions Asos.OpenTelemetry/Asos.OpenTelemetry.AspNetCore/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Open Telemetry extensions for Asp Net Core

A library for configuring OpenTelemetry in ASP.NET Core applications,

## What's it for?

This library is intended to help modify the default behaviour of OpenTelemetry in ASP.NET Core applications, allowing
some customisation of the way data is exported, sampled and other behaviours.

## How does it work?

Extension methods are available that allow you to change the behaviour of OpenTelemetry via the WebApplicationBuilder

```csharp
builder.ConfigureOpenTelemetryCustomSampling(
options =>
{
// whatever options you want to set
});
```

The `ConfigureOpenTelemetryCustomSampling` method allows you to set up custom sampling rules, which can be used to control
the sampling rate of different routes or HTTP methods in your application.

To define the rules, create a section in your `appsettings.json` file under the `OpenTelemetry:Sampling` path.

```json
{
"OpenTelemetry": {
"Sampling": {
"DefaultRate": 0.05,
"SamplingRules": [
{
"RoutePattern": "^/api/customers/\\d+$",
"Method": "GET",
"Rate": 1.0
},
{
"RoutePattern": "^/api/orders$",
"Method": "POST",
"Rate": 0.25
},
{
"RoutePattern": "^/health$",
"Method": "GET",
"Rate": 0.0
}
]
}
}
}
```

By doing so, you can control the sampling rate for specific routes and HTTP methods in your ASP.NET Core application.

Be aware that different sampling rates can break the consistency of your traces, so use this feature with caution. It's a good
option when you don't call into external APIs and just call your own dependencies, as it can help reduce the amount of data
you produce

For example, if you have a GET endpoint that only calls a database and no other services, is successful a very high percentage of
time and you don't need to see every single request, you can set the sampling rate to 0.05 (5%) for that endpoint.

You might have another endpoint that performs a POST operation and calls into an external API, which is less reliable and you want to see
every request, so you can set the sampling rate to 1.0 (100%) for that endpoint.





Loading