From 2ff440b383dec0988f5714fac5784e5c5144d18e Mon Sep 17 00:00:00 2001 From: Stepan Grankin Date: Fri, 7 Aug 2026 22:16:16 +0300 Subject: [PATCH 1/7] =?UTF-8?q?refactor(telemetry):=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D1=85=D0=BE=D0=B4=20=D1=81=20OpenTracing/Jaeger/promethe?= =?UTF-8?q?us-net=20=D0=BD=D0=B0=20OpenTelemetry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Причина замены: клиент Jaeger для C# и сам OpenTracing давно архивированы, а prometheus-net 7 на .NET 10 ломает эндпоинт /metrics целиком — MeterAdapter падает с FormatException при разборе описаний встроенных инструментов рантайма. Трейсинг. Tracing переведён на System.Diagnostics.ActivitySource, имя источника вынесено в константу и передаётся в AddSource. Без этого активности создавались бы, но никуда не уезжали. Проверено против живого Jaeger с включённым OTLP: в сервисе FillInTheTextBot видны и серверные спаны ASP.NET, и собственные Before / ProcessIncomingAsync / GetResponseAsync / AfterAsync. Метрики. MetricsCollector переведён на System.Diagnostics.Metrics. Взят ObservableGauge, а не Counter: counter экспортировался бы как metrics_total и сломал бы существующие дашборды. Проверено на живом эндпоинте — метрика отдаётся как metrics{metric_name=...,parameter=...}, то есть ровно как раньше. Экспортёр дополнительно добавляет метку otel_scope_name, имя и остальные метки не меняются. Метрики HTTP-слоя меняют имена: вместо http_requests_received_total и http_request_duration_seconds от prometheus-net приходят http_server_request_duration_seconds и http_server_active_requests по соглашениям OpenTelemetry. Дашборды по ним нужно поправить. Адрес коллектора берётся из существующей секции Tracing, порт по умолчанию 4317 (OTLP gRPC вместо UDP-порта агента Jaeger 6831). Если Host пустой, экспортёр трейсов не подключается. Проект Services больше не зависит ни от одного пакета телеметрии — ActivitySource и Meter входят в состав рантайма. --- src/Directory.Packages.props | 13 ++-- .../DI/ExternalServicesRegistration.cs | 35 ---------- .../FillInTheTextBot.Api.csproj | 10 +-- src/FillInTheTextBot.Api/Startup.cs | 69 +++++++++++++++++-- .../MessengerService.cs | 6 +- .../DialogflowService.cs | 2 +- .../FillInTheTextBot.Services.csproj | 2 - .../MetricsCollector.cs | 65 +++++++++++++---- src/FillInTheTextBot.Services/Tracing.cs | 32 ++++++--- 9 files changed, 155 insertions(+), 79 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index eeb0f2b4..6dcb0980 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -18,12 +18,13 @@ - - - - - - + + + + + + + diff --git a/src/FillInTheTextBot.Api/DI/ExternalServicesRegistration.cs b/src/FillInTheTextBot.Api/DI/ExternalServicesRegistration.cs index 49a3d798..1a2810f7 100644 --- a/src/FillInTheTextBot.Api/DI/ExternalServicesRegistration.cs +++ b/src/FillInTheTextBot.Api/DI/ExternalServicesRegistration.cs @@ -1,21 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using FillInTheTextBot.Services.Configuration; using Google.Apis.Auth.OAuth2; using Google.Cloud.Dialogflow.V2; using GranSteL.Helpers.Redis; using GranSteL.Tools.ScopeSelector; using Grpc.Auth; -using Jaeger; -using Jaeger.Reporters; -using Jaeger.Samplers; -using Jaeger.Senders.Thrift; -using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; -using OpenTracing; -using OpenTracing.Util; using StackExchange.Redis; namespace FillInTheTextBot.Api.DI @@ -27,7 +19,6 @@ internal static void AddExternalServices(this IServiceCollection services) services.AddSingleton(RegisterSessionsClientScopes); services.AddSingleton(RegisterContextsClientScopes); services.AddSingleton(RegisterRedisClient); - services.AddSingleton(RegisterTracer); services.AddSingleton(RegisterCacheService); } @@ -147,32 +138,6 @@ private static IDatabase RegisterRedisClient(IServiceProvider provider) return dataBase; } - private static ITracer RegisterTracer(IServiceProvider provider) - { - var env = provider.GetService(); - // TODO: get config as parameter - var configuration = provider.GetService(); - - var serviceName = env.ApplicationName; - var fullVersion = Assembly.GetExecutingAssembly().GetName().Version; - - var version = $"{fullVersion?.Major}.{fullVersion?.Minor}.{fullVersion?.Build}"; - - var sampler = new ConstSampler(true); - var reporter = new RemoteReporter.Builder() - .WithSender(new UdpSender(configuration.Host, configuration.Port, 0)) - .Build(); - - var tracer = new Tracer.Builder(serviceName) - .WithSampler(sampler) - .WithReporter(reporter) - .WithTag("Version", version) - .Build(); - - GlobalTracer.Register(tracer); - return tracer; - } - private static IRedisCacheService RegisterCacheService(IServiceProvider provider) { var configuration = provider.GetService(); diff --git a/src/FillInTheTextBot.Api/FillInTheTextBot.Api.csproj b/src/FillInTheTextBot.Api/FillInTheTextBot.Api.csproj index 5462fab1..36d52f09 100644 --- a/src/FillInTheTextBot.Api/FillInTheTextBot.Api.csproj +++ b/src/FillInTheTextBot.Api/FillInTheTextBot.Api.csproj @@ -8,12 +8,14 @@ - - - - + + + + + + diff --git a/src/FillInTheTextBot.Api/Startup.cs b/src/FillInTheTextBot.Api/Startup.cs index 60ad0696..2daef83e 100644 --- a/src/FillInTheTextBot.Api/Startup.cs +++ b/src/FillInTheTextBot.Api/Startup.cs @@ -6,13 +6,18 @@ using Microsoft.Extensions.DependencyInjection; using System; using System.Linq; +using System.Reflection; using FillInTheTextBot.Api.DI; -using Prometheus; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; namespace FillInTheTextBot.Api { public class Startup { + private const int DefaultOtlpPort = 4317; + private readonly IConfiguration _configuration; public Startup(IConfiguration configuration) @@ -28,7 +33,8 @@ public void ConfigureServices(IServiceCollection services) .AddMvc() .AddNewtonsoftJson(); - services.AddOpenTracing(); + AddTelemetry(services); + services.AddHttpLogging(o => { o.LoggingFields = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.All; @@ -39,6 +45,61 @@ public void ConfigureServices(IServiceCollection services) services.AddExternalServices(); } + private void AddTelemetry(IServiceCollection services) + { + var fullVersion = Assembly.GetExecutingAssembly().GetName().Version; + var version = $"{fullVersion?.Major}.{fullVersion?.Minor}.{fullVersion?.Build}"; + + var otlpEndpoint = GetOtlpEndpoint(); + + services.AddOpenTelemetry() + .ConfigureResource(resource => resource.AddService("FillInTheTextBot", serviceVersion: version)) + .WithTracing(builder => + { + builder + // Без AddSource активности из Tracing создаются, но не экспортируются + .AddSource(Tracing.ActivitySourceName) + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); + + if (otlpEndpoint is not null) + { + builder.AddOtlpExporter(options => options.Endpoint = otlpEndpoint); + } + }) + .WithMetrics(builder => builder + .AddMeter(MetricsCollector.MeterName) + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation() + .AddPrometheusExporter()); + } + + /// + /// Адрес OTLP-коллектора. Если хост не задан, экспорт трейсов не включается — + /// иначе экспортёр будет циклически долбиться в несуществующий адрес. + /// + private Uri GetOtlpEndpoint() + { + // Значения читаются как строки, а не через Get: в шаблонном + // appsettings.json Port пустой, и типизированная привязка на нём падает + var tracing = _configuration.GetSection($"{nameof(AppConfiguration)}:{nameof(AppConfiguration.Tracing)}"); + + var host = tracing[nameof(TracingConfiguration.Host)]; + + if (string.IsNullOrWhiteSpace(host)) + { + return null; + } + + var port = int.TryParse(tracing[nameof(TracingConfiguration.Port)], out var configuredPort) && configuredPort > 0 + ? configuredPort + : DefaultOtlpPort; + + var endpoint = new Uri($"http://{host}:{port}"); + + return endpoint; + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // ReSharper disable once UnusedMember.Global @@ -47,8 +108,6 @@ public void Configure(IApplicationBuilder app, AppConfiguration configuration) app.UseMiddleware(); app.UseRouting(); - app.UseHttpMetrics(); - app.UseGrpcMetrics(); if (configuration.HttpLog.Enabled) { @@ -62,7 +121,7 @@ public void Configure(IApplicationBuilder app, AppConfiguration configuration) app.UseEndpoints(e => { e.MapControllers(); - e.MapMetrics(); + e.MapPrometheusScrapingEndpoint(); }); } } diff --git a/src/FillInTheTextBot.Messengers/MessengerService.cs b/src/FillInTheTextBot.Messengers/MessengerService.cs index bd2c0d4b..a5c6f79f 100644 --- a/src/FillInTheTextBot.Messengers/MessengerService.cs +++ b/src/FillInTheTextBot.Messengers/MessengerService.cs @@ -42,9 +42,9 @@ public virtual async Task ProcessIncomingAsync(TInput input) request = Before(input); } - using (Tracing.Trace(s => s - .WithTag(nameof(request.UserHash), request.UserHash) - .WithTag(nameof(request.SessionId), request.SessionId))) + using (Tracing.Trace(a => a + .SetTag(nameof(request.UserHash), request.UserHash) + .SetTag(nameof(request.SessionId), request.SessionId))) { var contexts = GetContexts(request); request.RequiredContexts.AddRange(contexts); diff --git a/src/FillInTheTextBot.Services/DialogflowService.cs b/src/FillInTheTextBot.Services/DialogflowService.cs index 6a8af60a..be3395e9 100644 --- a/src/FillInTheTextBot.Services/DialogflowService.cs +++ b/src/FillInTheTextBot.Services/DialogflowService.cs @@ -100,7 +100,7 @@ public Task SetContextAsync(string sessionId, string scopeKey, string contextNam private async Task GetResponseInternalAsync(InternalModels.Request request, SessionsClient client, ScopeContext context) { - using (Tracing.Trace(s => s.WithTag(nameof(context.ScopeId), context.ScopeId), "Get response from Dialogflow")) + using (Tracing.Trace(a => a.SetTag(nameof(context.ScopeId), context.ScopeId), "Get response from Dialogflow")) { MetricsCollector.Increment("dialogflow_DetectIntent_scope", context.ScopeId); diff --git a/src/FillInTheTextBot.Services/FillInTheTextBot.Services.csproj b/src/FillInTheTextBot.Services/FillInTheTextBot.Services.csproj index ba72e4b7..bb8a7d29 100644 --- a/src/FillInTheTextBot.Services/FillInTheTextBot.Services.csproj +++ b/src/FillInTheTextBot.Services/FillInTheTextBot.Services.csproj @@ -11,8 +11,6 @@ - - diff --git a/src/FillInTheTextBot.Services/MetricsCollector.cs b/src/FillInTheTextBot.Services/MetricsCollector.cs index 18e8e2bd..266ff6a0 100644 --- a/src/FillInTheTextBot.Services/MetricsCollector.cs +++ b/src/FillInTheTextBot.Services/MetricsCollector.cs @@ -1,19 +1,56 @@ -using Prometheus; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.Metrics; -namespace FillInTheTextBot.Services; - -public static class MetricsCollector +namespace FillInTheTextBot.Services { - private static readonly Gauge Metrics; - - static MetricsCollector() + public static class MetricsCollector { - Metrics = Prometheus.Metrics - .CreateGauge("metrics", "Custom metrics", "metric_name", "parameter"); - } + /// + /// Имя счётчика. Его нужно передать в AddMeter при настройке OpenTelemetry. + /// + public const string MeterName = "FillInTheTextBot"; - public static void Increment(string key, string value) - { - Metrics.WithLabels(key, value).Inc(); + private const string MetricName = "metrics"; + + private const string MetricNameLabel = "metric_name"; + private const string ParameterLabel = "parameter"; + + private static readonly Meter Meter; + + /// + /// Значения по комбинациям меток. Хранятся в памяти, потому что метрика отдаётся + /// как gauge — см. комментарий ниже. + /// + private static readonly ConcurrentDictionary<(string Key, string Value), long> Values = new(); + + static MetricsCollector() + { + Meter = new Meter(MeterName); + + // Раньше метрика собиралась prometheus-net как Gauge с именем "metrics" и метками + // metric_name/parameter, на которую опираются существующие дашборды и алерты. + // Counter в OpenTelemetry экспортировался бы как "metrics_total", поэтому здесь + // ObservableGauge: он отдаёт то же имя и те же метки. Смысл у значения при этом + // счётчиковый — только растёт. Переименование в честный counter сломает дашборды, + // поэтому делать его нужно отдельно и осознанно. + Meter.CreateObservableGauge(MetricName, GetMeasurements, description: "Custom metrics"); + } + + public static void Increment(string key, string value) + { + Values.AddOrUpdate((key, value), 1, (_, current) => current + 1); + } + + private static IEnumerable> GetMeasurements() + { + foreach (var pair in Values) + { + yield return new Measurement( + pair.Value, + new KeyValuePair(MetricNameLabel, pair.Key.Key), + new KeyValuePair(ParameterLabel, pair.Key.Value)); + } + } } -} \ No newline at end of file +} diff --git a/src/FillInTheTextBot.Services/Tracing.cs b/src/FillInTheTextBot.Services/Tracing.cs index 6642556f..0f4106e5 100644 --- a/src/FillInTheTextBot.Services/Tracing.cs +++ b/src/FillInTheTextBot.Services/Tracing.cs @@ -1,21 +1,35 @@ using System; +using System.Diagnostics; using System.Runtime.CompilerServices; -using OpenTracing; -using OpenTracing.Util; namespace FillInTheTextBot.Services { public static class Tracing { - public static IScope Trace(Action spanBuilderAction = null, string operationName = null, [CallerMemberName] string caller = null) - { - var spanBuilder = GlobalTracer.Instance.BuildSpan(operationName ?? caller); + /// + /// Имя источника активностей. Его нужно передать в AddSource при настройке + /// OpenTelemetry, иначе активности будут создаваться, но никуда не уедут. + /// + public const string ActivitySourceName = "FillInTheTextBot"; + + private static readonly ActivitySource ActivitySource = new(ActivitySourceName); - spanBuilderAction?.Invoke(spanBuilder); + /// + /// Открывает активность. Если слушателей нет (юнит-тесты, отключённый экспорт), + /// StartActivity возвращает null — using с null работает штатно, а действие + /// над активностью не вызывается. + /// + public static Activity Trace(Action activityAction = null, string operationName = null, + [CallerMemberName] string caller = null) + { + var activity = ActivitySource.StartActivity(operationName ?? caller); - var scope = spanBuilder.StartActive(true); + if (activity is not null) + { + activityAction?.Invoke(activity); + } - return scope; + return activity; } } -} \ No newline at end of file +} From 3565a7a0dc184102244f7c52dc7ba01f0f66728f Mon Sep 17 00:00:00 2001 From: Stepan Grankin Date: Sun, 9 Aug 2026 13:02:19 +0300 Subject: [PATCH 2/7] TelemetryRegistration --- .../DI/TelemetryRegistration.cs | 69 +++++++++++++++++++ src/FillInTheTextBot.Api/Startup.cs | 65 +---------------- .../Configuration/TracingConfiguration.cs | 2 +- 3 files changed, 71 insertions(+), 65 deletions(-) create mode 100644 src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs diff --git a/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs b/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs new file mode 100644 index 00000000..0ee00e69 --- /dev/null +++ b/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs @@ -0,0 +1,69 @@ +using System; +using System.Reflection; +using FillInTheTextBot.Services; +using FillInTheTextBot.Services.Configuration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace FillInTheTextBot.Api.DI +{ + internal static class TelemetryRegistration + { + private const int DefaultOtlpPort = 4317; + + internal static void AddTelemetry(this IServiceCollection services, IConfiguration configuration) + { + var fullVersion = Assembly.GetExecutingAssembly().GetName().Version; + var version = $"{fullVersion?.Major}.{fullVersion?.Minor}.{fullVersion?.Build}"; + + var otlpEndpoint = GetOtlpEndpoint(configuration); + + services.AddOpenTelemetry() + .ConfigureResource(resource => resource.AddService("FillInTheTextBot", serviceVersion: version)) + .WithTracing(builder => + { + builder + // Без AddSource активности из Tracing создаются, но не экспортируются + .AddSource(Tracing.ActivitySourceName) + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); + + if (otlpEndpoint is not null) + { + builder.AddOtlpExporter(options => options.Endpoint = otlpEndpoint); + } + }) + .WithMetrics(builder => builder + .AddMeter(MetricsCollector.MeterName) + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation() + .AddPrometheusExporter()); + } + + /// + /// Адрес OTLP-коллектора. Если хост не задан, экспорт трейсов не включается — + /// иначе экспортёр будет циклически долбиться в несуществующий адрес. + /// + private static Uri GetOtlpEndpoint(IConfiguration configuration) + { + var tracing = configuration + .GetSection($"{nameof(AppConfiguration)}:{nameof(AppConfiguration.Tracing)}") + .Get(); + + if (string.IsNullOrWhiteSpace(tracing?.Host)) + { + return null; + } + + var port = tracing.Port is > 0 ? tracing.Port.Value : DefaultOtlpPort; + + var endpoint = new Uri($"http://{tracing.Host}:{port}"); + + return endpoint; + } + } +} diff --git a/src/FillInTheTextBot.Api/Startup.cs b/src/FillInTheTextBot.Api/Startup.cs index 2daef83e..cdf3d267 100644 --- a/src/FillInTheTextBot.Api/Startup.cs +++ b/src/FillInTheTextBot.Api/Startup.cs @@ -1,23 +1,16 @@ using FillInTheTextBot.Api.Middleware; -using FillInTheTextBot.Services; using FillInTheTextBot.Services.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using System; using System.Linq; -using System.Reflection; using FillInTheTextBot.Api.DI; -using OpenTelemetry.Metrics; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; namespace FillInTheTextBot.Api { public class Startup { - private const int DefaultOtlpPort = 4317; - private readonly IConfiguration _configuration; public Startup(IConfiguration configuration) @@ -33,7 +26,7 @@ public void ConfigureServices(IServiceCollection services) .AddMvc() .AddNewtonsoftJson(); - AddTelemetry(services); + services.AddTelemetry(_configuration); services.AddHttpLogging(o => { @@ -45,62 +38,6 @@ public void ConfigureServices(IServiceCollection services) services.AddExternalServices(); } - private void AddTelemetry(IServiceCollection services) - { - var fullVersion = Assembly.GetExecutingAssembly().GetName().Version; - var version = $"{fullVersion?.Major}.{fullVersion?.Minor}.{fullVersion?.Build}"; - - var otlpEndpoint = GetOtlpEndpoint(); - - services.AddOpenTelemetry() - .ConfigureResource(resource => resource.AddService("FillInTheTextBot", serviceVersion: version)) - .WithTracing(builder => - { - builder - // Без AddSource активности из Tracing создаются, но не экспортируются - .AddSource(Tracing.ActivitySourceName) - .AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation(); - - if (otlpEndpoint is not null) - { - builder.AddOtlpExporter(options => options.Endpoint = otlpEndpoint); - } - }) - .WithMetrics(builder => builder - .AddMeter(MetricsCollector.MeterName) - .AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddRuntimeInstrumentation() - .AddPrometheusExporter()); - } - - /// - /// Адрес OTLP-коллектора. Если хост не задан, экспорт трейсов не включается — - /// иначе экспортёр будет циклически долбиться в несуществующий адрес. - /// - private Uri GetOtlpEndpoint() - { - // Значения читаются как строки, а не через Get: в шаблонном - // appsettings.json Port пустой, и типизированная привязка на нём падает - var tracing = _configuration.GetSection($"{nameof(AppConfiguration)}:{nameof(AppConfiguration.Tracing)}"); - - var host = tracing[nameof(TracingConfiguration.Host)]; - - if (string.IsNullOrWhiteSpace(host)) - { - return null; - } - - var port = int.TryParse(tracing[nameof(TracingConfiguration.Port)], out var configuredPort) && configuredPort > 0 - ? configuredPort - : DefaultOtlpPort; - - var endpoint = new Uri($"http://{host}:{port}"); - - return endpoint; - } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // ReSharper disable once UnusedMember.Global public void Configure(IApplicationBuilder app, AppConfiguration configuration) diff --git a/src/FillInTheTextBot.Services/Configuration/TracingConfiguration.cs b/src/FillInTheTextBot.Services/Configuration/TracingConfiguration.cs index 84a19121..3b0b7895 100644 --- a/src/FillInTheTextBot.Services/Configuration/TracingConfiguration.cs +++ b/src/FillInTheTextBot.Services/Configuration/TracingConfiguration.cs @@ -4,6 +4,6 @@ public class TracingConfiguration : Configuration { public string Host { get; set; } - public int Port { get; set; } + public int? Port { get; set; } } } \ No newline at end of file From e3422e8231d01a546b8291b911dd28b301757292 Mon Sep 17 00:00:00 2001 From: Stepan Grankin Date: Sun, 9 Aug 2026 13:41:49 +0300 Subject: [PATCH 3/7] simplified version --- src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs b/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs index 0ee00e69..f7147748 100644 --- a/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs +++ b/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs @@ -16,8 +16,7 @@ internal static class TelemetryRegistration internal static void AddTelemetry(this IServiceCollection services, IConfiguration configuration) { - var fullVersion = Assembly.GetExecutingAssembly().GetName().Version; - var version = $"{fullVersion?.Major}.{fullVersion?.Minor}.{fullVersion?.Build}"; + var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString(3); var otlpEndpoint = GetOtlpEndpoint(configuration); From a5fdfe36ad4e32a108d945f3bd5473942c782605 Mon Sep 17 00:00:00 2001 From: Stepan Grankin Date: Sun, 9 Aug 2026 14:22:17 +0300 Subject: [PATCH 4/7] assemblyName --- src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs b/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs index f7147748..d1e831e8 100644 --- a/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs +++ b/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs @@ -16,12 +16,13 @@ internal static class TelemetryRegistration internal static void AddTelemetry(this IServiceCollection services, IConfiguration configuration) { - var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString(3); + var assemblyName = Assembly.GetExecutingAssembly().GetName(); + var version = assemblyName.Version?.ToString(3); var otlpEndpoint = GetOtlpEndpoint(configuration); services.AddOpenTelemetry() - .ConfigureResource(resource => resource.AddService("FillInTheTextBot", serviceVersion: version)) + .ConfigureResource(resource => resource.AddService(assemblyName.Name, serviceVersion: version)) .WithTracing(builder => { builder From e0a571db6f1ee0411f09954bd9e4120edd45b3c3 Mon Sep 17 00:00:00 2001 From: Stepan Grankin Date: Sun, 9 Aug 2026 14:42:38 +0300 Subject: [PATCH 5/7] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DI/ConfigurationRegistration.cs | 5 +---- .../DI/TelemetryRegistration.cs | 15 ++++----------- src/FillInTheTextBot.Api/Startup.cs | 6 ++++-- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/FillInTheTextBot.Api/DI/ConfigurationRegistration.cs b/src/FillInTheTextBot.Api/DI/ConfigurationRegistration.cs index 97602b58..a07a2864 100644 --- a/src/FillInTheTextBot.Api/DI/ConfigurationRegistration.cs +++ b/src/FillInTheTextBot.Api/DI/ConfigurationRegistration.cs @@ -1,15 +1,12 @@ using FillInTheTextBot.Services.Configuration; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace FillInTheTextBot.Api.DI { internal static class ConfigurationRegistration { - internal static void AddAppConfiguration(this IServiceCollection services, IConfiguration appConfiguration) + internal static void AddAppConfiguration(this IServiceCollection services, AppConfiguration configuration) { - var configuration = appConfiguration.GetSection($"{nameof(AppConfiguration)}").Get(); - services.AddSingleton(configuration); services.AddSingleton(configuration.HttpLog); services.AddSingleton(configuration.Redis); diff --git a/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs b/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs index d1e831e8..0c7897f5 100644 --- a/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs +++ b/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs @@ -2,7 +2,6 @@ using System.Reflection; using FillInTheTextBot.Services; using FillInTheTextBot.Services.Configuration; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; @@ -14,12 +13,12 @@ internal static class TelemetryRegistration { private const int DefaultOtlpPort = 4317; - internal static void AddTelemetry(this IServiceCollection services, IConfiguration configuration) + internal static void AddTelemetry(this IServiceCollection services, TracingConfiguration tracing) { var assemblyName = Assembly.GetExecutingAssembly().GetName(); var version = assemblyName.Version?.ToString(3); - var otlpEndpoint = GetOtlpEndpoint(configuration); + var otlpEndpoint = GetOtlpEndpoint(tracing); services.AddOpenTelemetry() .ConfigureResource(resource => resource.AddService(assemblyName.Name, serviceVersion: version)) @@ -48,12 +47,8 @@ internal static void AddTelemetry(this IServiceCollection services, IConfigurati /// Адрес OTLP-коллектора. Если хост не задан, экспорт трейсов не включается — /// иначе экспортёр будет циклически долбиться в несуществующий адрес. /// - private static Uri GetOtlpEndpoint(IConfiguration configuration) + private static Uri GetOtlpEndpoint(TracingConfiguration tracing) { - var tracing = configuration - .GetSection($"{nameof(AppConfiguration)}:{nameof(AppConfiguration.Tracing)}") - .Get(); - if (string.IsNullOrWhiteSpace(tracing?.Host)) { return null; @@ -61,9 +56,7 @@ private static Uri GetOtlpEndpoint(IConfiguration configuration) var port = tracing.Port is > 0 ? tracing.Port.Value : DefaultOtlpPort; - var endpoint = new Uri($"http://{tracing.Host}:{port}"); - - return endpoint; + return new UriBuilder(Uri.UriSchemeHttp, tracing.Host, port).Uri; } } } diff --git a/src/FillInTheTextBot.Api/Startup.cs b/src/FillInTheTextBot.Api/Startup.cs index cdf3d267..19d26b99 100644 --- a/src/FillInTheTextBot.Api/Startup.cs +++ b/src/FillInTheTextBot.Api/Startup.cs @@ -22,18 +22,20 @@ public Startup(IConfiguration configuration) // ReSharper disable once UnusedMember.Global public void ConfigureServices(IServiceCollection services) { + var appConfiguration = _configuration.GetSection(nameof(AppConfiguration)).Get(); + services .AddMvc() .AddNewtonsoftJson(); - services.AddTelemetry(_configuration); + services.AddTelemetry(appConfiguration.Tracing); services.AddHttpLogging(o => { o.LoggingFields = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.All; }); - services.AddAppConfiguration(_configuration); + services.AddAppConfiguration(appConfiguration); services.AddInternalServices(); services.AddExternalServices(); } From 701e7a66261903d3e353da15a5c451d250b213fc Mon Sep 17 00:00:00 2001 From: Stepan Grankin Date: Sun, 9 Aug 2026 16:29:38 +0300 Subject: [PATCH 6/7] Tracing Enabled config, false by default --- .../DI/TelemetryRegistration.cs | 13 ++++++++++--- src/FillInTheTextBot.Api/appsettings.json | 3 ++- .../Configuration/TracingConfiguration.cs | 2 ++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs b/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs index 0c7897f5..5e98699d 100644 --- a/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs +++ b/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs @@ -44,16 +44,23 @@ internal static void AddTelemetry(this IServiceCollection services, TracingConfi } /// - /// Адрес OTLP-коллектора. Если хост не задан, экспорт трейсов не включается — - /// иначе экспортёр будет циклически долбиться в несуществующий адрес. + /// Адрес OTLP-коллектора. Трейсинг включается только явным флагом Enabled — + /// иначе экспортёр не добавляется, чтобы не долбиться в несуществующий адрес. /// private static Uri GetOtlpEndpoint(TracingConfiguration tracing) { - if (string.IsNullOrWhiteSpace(tracing?.Host)) + if (tracing is not { Enabled: true }) { return null; } + if (string.IsNullOrWhiteSpace(tracing.Host)) + { + throw new InvalidOperationException( + $"{nameof(TracingConfiguration)}.{nameof(TracingConfiguration.Host)} обязателен, " + + $"когда трейсинг включён ({nameof(TracingConfiguration.Enabled)} = true)."); + } + var port = tracing.Port is > 0 ? tracing.Port.Value : DefaultOtlpPort; return new UriBuilder(Uri.UriSchemeHttp, tracing.Host, port).Uri; diff --git a/src/FillInTheTextBot.Api/appsettings.json b/src/FillInTheTextBot.Api/appsettings.json index fc18875b..35b45ecb 100644 --- a/src/FillInTheTextBot.Api/appsettings.json +++ b/src/FillInTheTextBot.Api/appsettings.json @@ -28,8 +28,9 @@ "KeyPrefix": "" }, "Tracing": { + "Enabled": false, "Host": "", - "Port": "" + "Port": "" }, "Conversation":{ "ResetContextWords": [ diff --git a/src/FillInTheTextBot.Services/Configuration/TracingConfiguration.cs b/src/FillInTheTextBot.Services/Configuration/TracingConfiguration.cs index 3b0b7895..eeb9b517 100644 --- a/src/FillInTheTextBot.Services/Configuration/TracingConfiguration.cs +++ b/src/FillInTheTextBot.Services/Configuration/TracingConfiguration.cs @@ -2,6 +2,8 @@ { public class TracingConfiguration : Configuration { + public bool Enabled { get; set; } + public string Host { get; set; } public int? Port { get; set; } From 4c7e2a589114b148ebf6af3db54c8b031542ddfa Mon Sep 17 00:00:00 2001 From: Stepan Grankin Date: Sun, 9 Aug 2026 16:36:41 +0300 Subject: [PATCH 7/7] Telemetry ScopeName const --- src/FillInTheTextBot.Services/MetricsCollector.cs | 2 +- src/FillInTheTextBot.Services/Telemetry.cs | 12 ++++++++++++ src/FillInTheTextBot.Services/Tracing.cs | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 src/FillInTheTextBot.Services/Telemetry.cs diff --git a/src/FillInTheTextBot.Services/MetricsCollector.cs b/src/FillInTheTextBot.Services/MetricsCollector.cs index 266ff6a0..ede8e38e 100644 --- a/src/FillInTheTextBot.Services/MetricsCollector.cs +++ b/src/FillInTheTextBot.Services/MetricsCollector.cs @@ -9,7 +9,7 @@ public static class MetricsCollector /// /// Имя счётчика. Его нужно передать в AddMeter при настройке OpenTelemetry. /// - public const string MeterName = "FillInTheTextBot"; + public const string MeterName = Telemetry.ScopeName; private const string MetricName = "metrics"; diff --git a/src/FillInTheTextBot.Services/Telemetry.cs b/src/FillInTheTextBot.Services/Telemetry.cs new file mode 100644 index 00000000..f5f8ccd3 --- /dev/null +++ b/src/FillInTheTextBot.Services/Telemetry.cs @@ -0,0 +1,12 @@ +namespace FillInTheTextBot.Services +{ + /// + /// Общие константы телеметрии. Единое имя, под которым приложение публикует + /// активности (ActivitySource) и метрики (Meter) — в OpenTelemetry это + /// instrumentation scope (otel_scope_name). + /// + public static class Telemetry + { + public const string ScopeName = "FillInTheTextBot"; + } +} diff --git a/src/FillInTheTextBot.Services/Tracing.cs b/src/FillInTheTextBot.Services/Tracing.cs index 0f4106e5..026b9384 100644 --- a/src/FillInTheTextBot.Services/Tracing.cs +++ b/src/FillInTheTextBot.Services/Tracing.cs @@ -10,7 +10,7 @@ public static class Tracing /// Имя источника активностей. Его нужно передать в AddSource при настройке /// OpenTelemetry, иначе активности будут создаваться, но никуда не уедут. /// - public const string ActivitySourceName = "FillInTheTextBot"; + public const string ActivitySourceName = Telemetry.ScopeName; private static readonly ActivitySource ActivitySource = new(ActivitySourceName);