diff --git a/AspNetCore.Examples.OpenTelemetry.Api/AspNetCore.Examples.OpenTelemetry.Api.csproj b/AspNetCore.Examples.OpenTelemetry.Api/AspNetCore.Examples.OpenTelemetry.Api.csproj index 68b458b..e36933c 100644 --- a/AspNetCore.Examples.OpenTelemetry.Api/AspNetCore.Examples.OpenTelemetry.Api.csproj +++ b/AspNetCore.Examples.OpenTelemetry.Api/AspNetCore.Examples.OpenTelemetry.Api.csproj @@ -7,7 +7,7 @@ - + diff --git a/AspNetCore.Examples.OpenTelemetry.Api/WeatherForecast/WeatherForecastExtensions.cs b/AspNetCore.Examples.OpenTelemetry.Api/WeatherForecast/Endpoints/Get.cs similarity index 50% rename from AspNetCore.Examples.OpenTelemetry.Api/WeatherForecast/WeatherForecastExtensions.cs rename to AspNetCore.Examples.OpenTelemetry.Api/WeatherForecast/Endpoints/Get.cs index 1c63f92..d263001 100644 --- a/AspNetCore.Examples.OpenTelemetry.Api/WeatherForecast/WeatherForecastExtensions.cs +++ b/AspNetCore.Examples.OpenTelemetry.Api/WeatherForecast/Endpoints/Get.cs @@ -1,32 +1,27 @@ -using System.Diagnostics; +using AspNetCore.Examples.OpenTelemetry.Api.WeatherForecast.Services; +using System.Diagnostics; -namespace AspNetCore.Examples.OpenTelemetry.Api.WeatherForecast; +namespace AspNetCore.Examples.OpenTelemetry.Api.WeatherForecast.Endpoints; -internal static class WeatherForecastExtensions +internal static class Get { - public static IServiceCollection AddWeatherForecast(this IServiceCollection services) - { - services.AddTelemetry(); - return services; - } + public static readonly IReadOnlyList _summaries = + [ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + ]; - public static IEndpointRouteBuilder MapWeatherForecast(this IEndpointRouteBuilder endpoints) + public static IEndpointRouteBuilder MapGet(this IEndpointRouteBuilder endpoints) { - var summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - endpoints.MapGet("/weather-forecast", (IWeatherForecastTelemetry telemetry) => + endpoints.MapGet("/weather-forecast", (ITelemetry telemetry) => { using var activity = telemetry.ActivitySource.StartActivity(name: "weather_forecast.request", kind: ActivityKind.Internal); - + var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast ( DateOnly.FromDateTime(DateTime.Now.AddDays(index)), Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] + _summaries[Random.Shared.Next(_summaries.Count)] )) .ToArray(); diff --git a/AspNetCore.Examples.OpenTelemetry.Api/WeatherForecast/Extensions.cs b/AspNetCore.Examples.OpenTelemetry.Api/WeatherForecast/Extensions.cs new file mode 100644 index 0000000..79337b7 --- /dev/null +++ b/AspNetCore.Examples.OpenTelemetry.Api/WeatherForecast/Extensions.cs @@ -0,0 +1,19 @@ +using AspNetCore.Examples.OpenTelemetry.Api.WeatherForecast.Endpoints; +using AspNetCore.Examples.OpenTelemetry.Api.WeatherForecast.Services; + +namespace AspNetCore.Examples.OpenTelemetry.Api.WeatherForecast; + +internal static class Extensions +{ + public static IServiceCollection AddWeatherForecast(this IServiceCollection services) + { + services.AddTelemetry(); + return services; + } + + public static IEndpointRouteBuilder MapWeatherForecast(this IEndpointRouteBuilder endpoints) + { + endpoints.MapGet(); + return endpoints; + } +} diff --git a/AspNetCore.Examples.OpenTelemetry.Api/WeatherForecast/Services/Telemetry.cs b/AspNetCore.Examples.OpenTelemetry.Api/WeatherForecast/Services/Telemetry.cs new file mode 100644 index 0000000..6fbaca0 --- /dev/null +++ b/AspNetCore.Examples.OpenTelemetry.Api/WeatherForecast/Services/Telemetry.cs @@ -0,0 +1,32 @@ +using System.Diagnostics; +using System.Diagnostics.Metrics; + +namespace AspNetCore.Examples.OpenTelemetry.Api.WeatherForecast.Services; + +internal interface ITelemetry : ITelemetry +{ + Histogram TemperatureC { get; } +} + +internal class Telemetry : Telemetry, ITelemetry +{ + public Telemetry(ILoggerFactory loggerFactory, IMeterFactory meterFactory) + : base(loggerFactory, meterFactory) + { + TemperatureC = Meter.CreateHistogram("temperature", unit: "ºC"); + } + + public Histogram TemperatureC { get; } +} + +internal static class TelemetryExtensions +{ + public static IServiceCollection AddTelemetry(this IServiceCollection services) + { + services.AddSingleton(); + services.AddOpenTelemetry() + .WithTracing(t => t.AddSource(Telemetry.CategoryName)) + .WithMetrics(t => t.AddMeter(Telemetry.CategoryName)); + return services; + } +} diff --git a/AspNetCore.Examples.OpenTelemetry.Api/WeatherForecast/WeatherForecastTelemetry.cs b/AspNetCore.Examples.OpenTelemetry.Api/WeatherForecast/WeatherForecastTelemetry.cs deleted file mode 100644 index 7415eec..0000000 --- a/AspNetCore.Examples.OpenTelemetry.Api/WeatherForecast/WeatherForecastTelemetry.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Diagnostics; -using System.Diagnostics.Metrics; - -namespace AspNetCore.Examples.OpenTelemetry.Api.WeatherForecast; - -internal interface IWeatherForecastTelemetry : ITelemetry -{ - Histogram TemperatureC { get; } -} - -internal class WeatherForecastTelemetry : Telemetry, IWeatherForecastTelemetry -{ - public WeatherForecastTelemetry(ILoggerFactory loggerFactory, IMeterFactory meterFactory) - : base(loggerFactory, meterFactory) - { - TemperatureC = Meter.CreateHistogram("temperature", unit: "ºC"); - } - - public Histogram TemperatureC { get; } -} diff --git a/AspNetCore.Examples.OpenTelemetry.AspireHost/AspNetCore.Examples.OpenTelemetry.AspireHost.csproj b/AspNetCore.Examples.OpenTelemetry.AspireHost/AspNetCore.Examples.OpenTelemetry.AspireHost.csproj index 65b1d67..2f75535 100644 --- a/AspNetCore.Examples.OpenTelemetry.AspireHost/AspNetCore.Examples.OpenTelemetry.AspireHost.csproj +++ b/AspNetCore.Examples.OpenTelemetry.AspireHost/AspNetCore.Examples.OpenTelemetry.AspireHost.csproj @@ -1,8 +1,10 @@ - + + + Exe - net8.0 + net9.0 enable enable true @@ -10,8 +12,7 @@ - - + diff --git a/AspNetCore.Examples.OpenTelemetry.ServiceDefaults/AspNetCore.Examples.OpenTelemetry.ServiceDefaults.csproj b/AspNetCore.Examples.OpenTelemetry.ServiceDefaults/AspNetCore.Examples.OpenTelemetry.ServiceDefaults.csproj index 81b8b2b..d083ef7 100644 --- a/AspNetCore.Examples.OpenTelemetry.ServiceDefaults/AspNetCore.Examples.OpenTelemetry.ServiceDefaults.csproj +++ b/AspNetCore.Examples.OpenTelemetry.ServiceDefaults/AspNetCore.Examples.OpenTelemetry.ServiceDefaults.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/AspNetCore.Examples.OpenTelemetry.TelemetryExtensions/AspNetCore.Examples.OpenTelemetry.TelemetryExtensions.csproj b/AspNetCore.Examples.OpenTelemetry.TelemetryExtensions/AspNetCore.Examples.OpenTelemetry.TelemetryExtensions.csproj index 32c49c6..c11d698 100644 --- a/AspNetCore.Examples.OpenTelemetry.TelemetryExtensions/AspNetCore.Examples.OpenTelemetry.TelemetryExtensions.csproj +++ b/AspNetCore.Examples.OpenTelemetry.TelemetryExtensions/AspNetCore.Examples.OpenTelemetry.TelemetryExtensions.csproj @@ -7,8 +7,7 @@ - - + diff --git a/AspNetCore.Examples.OpenTelemetry.TelemetryExtensions/Extensions.cs b/AspNetCore.Examples.OpenTelemetry.TelemetryExtensions/Extensions.cs deleted file mode 100644 index a9b8cc0..0000000 --- a/AspNetCore.Examples.OpenTelemetry.TelemetryExtensions/Extensions.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; - -#pragma warning disable IDE0130 // Namespace does not match folder structure -namespace Microsoft.Extensions.Hosting; -#pragma warning restore IDE0130 // Namespace does not match folder structure -public static class Extensions -{ - private static readonly Type TelemetryType = typeof(Telemetry<>); - private static readonly Type ITelemetryType = typeof(ITelemetry<>); - - public static IServiceCollection AddTelemetry(this IServiceCollection services) - where TService : class - where TImplementation : class, TService - { - services.AddSingleton(); - services.AddTelemetryInternal(services => services.GetRequiredService()); - return services; - } - - public static IServiceCollection AddTelemetry(this IServiceCollection services) - where TService : class - { - services.AddSingleton(); - services.AddTelemetryInternal(services => services.GetRequiredService()); - return services; - } - - private static IServiceCollection AddTelemetryInternal(this IServiceCollection services, Func implementationFactory) - where TService : class - { - var implementationType = typeof(TService); - if (implementationType.TryGetBaseTelemetryType(out var baseTelemetryType)) - { - services.AddSingleton(baseTelemetryType, implementationFactory); - var categoryName = baseTelemetryType - .GetField(nameof(Telemetry.CategoryName), BindingFlags.Static | BindingFlags.NonPublic)? - .GetValue(obj: null) as string; - if (categoryName is not null) - { - services.AddOpenTelemetry() - .WithTracing(t => t.AddSource(categoryName)) - .WithMetrics(m => m.AddMeter(categoryName)); - } - } - foreach (var telemetryInterfaceType in implementationType.GetTelemetryInterfaceTypes()) - { - services.AddSingleton(telemetryInterfaceType, implementationFactory); - } - - return services; - } - - - private static bool TryGetBaseTelemetryType(this Type implementationType, [MaybeNullWhen(returnValue: false)] out Type baseTelemetryType) - { - baseTelemetryType = null; - Type? type = implementationType; - while (type != null && type != typeof(object)) - { - if (type.IsGenericType && type.GetGenericTypeDefinition() == TelemetryType) - { - baseTelemetryType = type; - return true; - } - type = type.BaseType; - } - return false; - } - - private static IEnumerable GetTelemetryInterfaceTypes(this Type implementationType) - { - return implementationType.GetInterfaces() - .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == ITelemetryType); - } -} diff --git a/AspNetCore.Examples.OpenTelemetry.TelemetryExtensions/Telemetry.cs b/AspNetCore.Examples.OpenTelemetry.TelemetryExtensions/Telemetry.cs index 31955a4..5d6295e 100644 --- a/AspNetCore.Examples.OpenTelemetry.TelemetryExtensions/Telemetry.cs +++ b/AspNetCore.Examples.OpenTelemetry.TelemetryExtensions/Telemetry.cs @@ -12,16 +12,32 @@ public class Telemetry : ITelemetry, IDisposable public static readonly string CategoryName = TypeNameHelper.GetTypeDisplayName(typeof(TCategoryName), includeGenericParameters: false, nestedTypeDelimiter: '.'); + private Lazy>? _logger; + private Lazy? _activitySource; + private Lazy? _meter; + public Telemetry(ILoggerFactory loggerFactory, IMeterFactory meterFactory) { - Logger = loggerFactory.CreateLogger(); - ActivitySource = new ActivitySource(ActivitySourceOptions.Name, ActivitySourceOptions.Version, ActivitySourceOptions.Tags); - Meter = meterFactory.Create(MeterOptions); + _logger = new Lazy>(() => + loggerFactory.CreateLogger(), + isThreadSafe: true); + _activitySource = new Lazy(() => + new ActivitySource(CategoryName, ActivitySourceOptions.Version, ActivitySourceOptions.Tags), + isThreadSafe: true); + _meter = new Lazy(() => + meterFactory.Create(new MeterOptions(CategoryName) + { + Version = MeterOptions.Version, + Tags = MeterOptions.Tags, + Scope = MeterOptions.Scope, + }), + isThreadSafe: true); } - public ILogger Logger { get; } - public ActivitySource ActivitySource { get; } - public Meter Meter { get; } + + public ILogger Logger => _logger?.Value ?? throw new ObjectDisposedException(nameof(Logger)); + public ActivitySource ActivitySource => _activitySource?.Value ?? throw new ObjectDisposedException(nameof(ActivitySource)); + public Meter Meter => _meter?.Value ?? throw new ObjectDisposedException(nameof(Meter)); protected virtual ActivitySourceOptions ActivitySourceOptions => new(CategoryName); protected virtual MeterOptions MeterOptions => new(CategoryName); @@ -32,9 +48,21 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - ActivitySource.Dispose(); - Meter.Dispose(); + Interlocked.Exchange(ref _logger, null); + + var activitySource = Interlocked.Exchange(ref _activitySource, null); + if (activitySource?.IsValueCreated ?? false) + { + activitySource.Value.Dispose(); + } + + var meter = Interlocked.Exchange(ref _meter, null); + if (meter?.IsValueCreated ?? false) + { + meter.Value.Dispose(); + } } + disposedValue = true; } } diff --git a/AspNetCore.Examples.OpenTelemetry.sln b/AspNetCore.Examples.OpenTelemetry.sln index 2d1f67d..4ae5647 100644 --- a/AspNetCore.Examples.OpenTelemetry.sln +++ b/AspNetCore.Examples.OpenTelemetry.sln @@ -8,6 +8,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A98F4720-450D-4CA0-B137-932A7FFA1F1A}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + global.json = global.json README.md = README.md EndProjectSection EndProject diff --git a/global.json b/global.json new file mode 100644 index 0000000..fc0162f --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "9.0.100-rc.2.24474.11" + } +} \ No newline at end of file