Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 7 additions & 6 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@
</ItemGroup>

<ItemGroup Label="Телеметрия">
<PackageVersion Include="Jaeger" Version="1.0.3" />
<PackageVersion Include="OpenTracing" Version="0.12.1" />
<PackageVersion Include="OpenTracing.Contrib.NetCore" Version="0.8.0" />
<PackageVersion Include="prometheus-net" Version="7.0.0" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="7.0.0" />
<PackageVersion Include="prometheus-net.AspNetCore.Grpc" Version="7.0.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.17.0" />
<!-- Экспортёр Prometheus у OpenTelemetry до сих пор выходит только в prerelease -->
<PackageVersion Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.17.0-beta.1" />
</ItemGroup>

<ItemGroup Label="Dialogflow и Redis">
Expand Down
5 changes: 1 addition & 4 deletions src/FillInTheTextBot.Api/DI/ConfigurationRegistration.cs
Original file line number Diff line number Diff line change
@@ -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<AppConfiguration>();

services.AddSingleton(configuration);
services.AddSingleton(configuration.HttpLog);
services.AddSingleton(configuration.Redis);
Expand Down
35 changes: 0 additions & 35 deletions src/FillInTheTextBot.Api/DI/ExternalServicesRegistration.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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);
}

Expand Down Expand Up @@ -147,32 +138,6 @@ private static IDatabase RegisterRedisClient(IServiceProvider provider)
return dataBase;
}

private static ITracer RegisterTracer(IServiceProvider provider)
{
var env = provider.GetService<IWebHostEnvironment>();
// TODO: get config as parameter
var configuration = provider.GetService<TracingConfiguration>();

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<RedisConfiguration>();
Expand Down
69 changes: 69 additions & 0 deletions src/FillInTheTextBot.Api/DI/TelemetryRegistration.cs
Original file line number Diff line number Diff line change
@@ -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());
}

/// <summary>
/// Адрес OTLP-коллектора. Трейсинг включается только явным флагом Enabled —
/// иначе экспортёр не добавляется, чтобы не долбиться в несуществующий адрес.
/// </summary>
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;
}
}
}
10 changes: 6 additions & 4 deletions src/FillInTheTextBot.Api/FillInTheTextBot.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Jaeger" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
<PackageReference Include="NLog.Web.AspNetCore" />
<PackageReference Include="OpenTracing.Contrib.NetCore" />
<PackageReference Include="prometheus-net.AspNetCore" />
<PackageReference Include="prometheus-net.AspNetCore.Grpc" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
</ItemGroup>

<ItemGroup>
Expand Down
14 changes: 6 additions & 8 deletions src/FillInTheTextBot.Api/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
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 FillInTheTextBot.Api.DI;
using Prometheus;

namespace FillInTheTextBot.Api
{
Expand All @@ -24,31 +22,31 @@ public Startup(IConfiguration configuration)
// ReSharper disable once UnusedMember.Global
public void ConfigureServices(IServiceCollection services)
{
var appConfiguration = _configuration.GetSection(nameof(AppConfiguration)).Get<AppConfiguration>();

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)
{
app.UseMiddleware<ExceptionsMiddleware>();

app.UseRouting();
app.UseHttpMetrics();
app.UseGrpcMetrics();

if (configuration.HttpLog.Enabled)
{
Expand All @@ -62,7 +60,7 @@ public void Configure(IApplicationBuilder app, AppConfiguration configuration)
app.UseEndpoints(e =>
{
e.MapControllers();
e.MapMetrics();
e.MapPrometheusScrapingEndpoint();
});
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/FillInTheTextBot.Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@
"KeyPrefix": ""
},
"Tracing": {
"Enabled": false,
"Host": "",
"Port": ""
"Port": ""
},
"Conversation":{
"ResetContextWords": [
Expand Down
6 changes: 3 additions & 3 deletions src/FillInTheTextBot.Messengers/MessengerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ public virtual async Task<TOutput> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
}
2 changes: 1 addition & 1 deletion src/FillInTheTextBot.Services/DialogflowService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public Task SetContextAsync(string sessionId, string scopeKey, string contextNam

private async Task<InternalModels.Dialog> 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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
<PackageReference Include="GranSteL.Tools.ScopeSelector" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="OpenTracing" />
<PackageReference Include="prometheus-net" />
<PackageReference Include="StackExchange.Redis" />
</ItemGroup>

Expand Down
65 changes: 51 additions & 14 deletions src/FillInTheTextBot.Services/MetricsCollector.cs
Original file line number Diff line number Diff line change
@@ -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");
}
/// <summary>
/// Имя счётчика. Его нужно передать в AddMeter при настройке OpenTelemetry.
/// </summary>
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;

/// <summary>
/// Значения по комбинациям меток. Хранятся в памяти, потому что метрика отдаётся
/// как gauge — см. комментарий ниже.
/// </summary>
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<Measurement<long>> GetMeasurements()
{
foreach (var pair in Values)
{
yield return new Measurement<long>(
pair.Value,
new KeyValuePair<string, object>(MetricNameLabel, pair.Key.Key),
new KeyValuePair<string, object>(ParameterLabel, pair.Key.Value));
}
}
}
}
}
12 changes: 12 additions & 0 deletions src/FillInTheTextBot.Services/Telemetry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace FillInTheTextBot.Services
{
/// <summary>
/// Общие константы телеметрии. Единое имя, под которым приложение публикует
/// активности (ActivitySource) и метрики (Meter) — в OpenTelemetry это
/// instrumentation scope (otel_scope_name).
/// </summary>
public static class Telemetry
{
public const string ScopeName = "FillInTheTextBot";
}
}
Loading
Loading