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/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/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/DI/TelemetryRegistration.cs b/src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs
new file mode 100644
index 00000000..5e98699d
--- /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.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, TracingConfiguration tracing)
+ {
+ var assemblyName = Assembly.GetExecutingAssembly().GetName();
+ var version = assemblyName.Version?.ToString(3);
+
+ var otlpEndpoint = GetOtlpEndpoint(tracing);
+
+ services.AddOpenTelemetry()
+ .ConfigureResource(resource => resource.AddService(assemblyName.Name, 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-коллектора. Трейсинг включается только явным флагом Enabled —
+ /// иначе экспортёр не добавляется, чтобы не долбиться в несуществующий адрес.
+ ///
+ private static Uri GetOtlpEndpoint(TracingConfiguration tracing)
+ {
+ 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/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..19d26b99 100644
--- a/src/FillInTheTextBot.Api/Startup.cs
+++ b/src/FillInTheTextBot.Api/Startup.cs
@@ -1,5 +1,4 @@
using FillInTheTextBot.Api.Middleware;
-using FillInTheTextBot.Services;
using FillInTheTextBot.Services.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
@@ -7,7 +6,6 @@
using System;
using System.Linq;
using FillInTheTextBot.Api.DI;
-using Prometheus;
namespace FillInTheTextBot.Api
{
@@ -24,22 +22,24 @@ 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.AddOpenTracing();
+ services.AddTelemetry(appConfiguration.Tracing);
+
services.AddHttpLogging(o =>
{
o.LoggingFields = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.All;
});
- services.AddAppConfiguration(_configuration);
+ services.AddAppConfiguration(appConfiguration);
services.AddInternalServices();
services.AddExternalServices();
}
-
// 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)
@@ -47,8 +47,6 @@ public void Configure(IApplicationBuilder app, AppConfiguration configuration)
app.UseMiddleware();
app.UseRouting();
- app.UseHttpMetrics();
- app.UseGrpcMetrics();
if (configuration.HttpLog.Enabled)
{
@@ -62,7 +60,7 @@ public void Configure(IApplicationBuilder app, AppConfiguration configuration)
app.UseEndpoints(e =>
{
e.MapControllers();
- e.MapMetrics();
+ e.MapPrometheusScrapingEndpoint();
});
}
}
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.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/Configuration/TracingConfiguration.cs b/src/FillInTheTextBot.Services/Configuration/TracingConfiguration.cs
index 84a19121..eeb9b517 100644
--- a/src/FillInTheTextBot.Services/Configuration/TracingConfiguration.cs
+++ b/src/FillInTheTextBot.Services/Configuration/TracingConfiguration.cs
@@ -2,8 +2,10 @@
{
public class TracingConfiguration : Configuration
{
+ public bool Enabled { get; set; }
+
public string Host { get; set; }
- public int Port { get; set; }
+ public int? Port { get; set; }
}
}
\ No newline at end of file
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..ede8e38e 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 = Telemetry.ScopeName;
- 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/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 6642556f..026b9384 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 = Telemetry.ScopeName;
+
+ 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
+}