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
35 changes: 21 additions & 14 deletions OBSERVABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,19 @@

## Архитектура сбора метрик

Метрики приложений построены на **OpenTelemetry** и доставляются в Prometheus
по **push-модели через Prometheus Pushgateway**.
Метрики приложений построены на **OpenTelemetry** и отдаются Prometheus
по **pull-модели**: каждый сервис публикует OpenTelemetry-метрики в формате Prometheus
на своём эндпоинте `/metrics`, а Prometheus скрейпит их напрямую.

- Scrapper: `http://scrapper:8081/metrics`
- Bot: `http://bot:8011/metrics` (отдельный Kestrel-эндпоинт, наружу не публикуется)

Эндпоинт `/metrics` исключён из общего rate limiter — иначе скрейп раз в 15 секунд
конкурировал бы за лимит с прикладным трафиком и метрики выглядели бы «пропавшими».

Имена серий, которые видит Prometheus, зафиксированы тестом
`MetricsEndpointTests` — экспортёр переименовывает инструменты, и панели Grafana
завязаны именно на итоговые имена.

## Метрики приложений

Expand Down Expand Up @@ -43,19 +54,16 @@

> Histogram-метрики дают в Prometheus три серии: `<name>_bucket`, `<name>_sum`, `<name>_count`.

## Конфигурация push
## Конфигурация сбора

Настройки секции `Telemetry:Pushgateway` (переопределяются переменными окружения
с разделителем `__`):
Настраивается на стороне Prometheus в `monitoring/prometheus.yml` — приложениям
никакой конфигурации телеметрии не требуется. Лейбл `job` берётся из имени scrape-job
(`scrapper` / `bot`), `instance` — из адреса цели.

| Ключ | Env-переменная | По умолчанию |
| Job | Цель | Интервал |
|---|---|---|
| `Endpoint` | `Telemetry__Pushgateway__Endpoint` | `http://pushgateway:9091/metrics` |
| `Job` | `Telemetry__Pushgateway__Job` | `scrapper` / `bot` |
| `Enabled` | `Telemetry__Pushgateway__Enabled` | `true` |
| `IntervalMilliseconds` | `Telemetry__Pushgateway__IntervalMilliseconds` | `5000` |

`Instance` берётся из переменной `HOSTNAME` (в контейнере — id контейнера) либо из имени хоста.
| `scrapper` | `scrapper:8081` | 15s (`global.scrape_interval`) |
| `bot` | `bot:8011` | 15s (`global.scrape_interval`) |

## Grafana

Expand All @@ -75,7 +83,7 @@
## Запуск мониторинга

```bash
# Поднять инфраструктуру (Kafka, Postgres, Valkey, Pushgateway, Prometheus, Grafana)
# Поднять инфраструктуру (Kafka, Postgres, Valkey, Prometheus, Grafana)
docker compose -f docker-compose.yml up -d

# Поднять приложения
Expand All @@ -84,5 +92,4 @@ docker compose -f docker-compose.yml -f docker-compose.apps.yml up -d

После запуска:
- Prometheus: http://localhost:9090
- Pushgateway: http://localhost:9091
- Grafana: http://localhost:3000 (admin / admin)
54 changes: 23 additions & 31 deletions docker-compose.apps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,45 +15,37 @@ services:
condition: service_started
valkey-cluster-init:
condition: service_completed_successfully
pushgateway:
condition: service_started
ports:
- "8081:8081"
- "8082:8082"
env_file:
- src/LinkTracker.Scrapper.Api/.env
environment:
ASPNETCORE_ENVIRONMENT: "Docker"
Telemetry__Pushgateway__Endpoint: "http://pushgateway:9091/metrics"
Telemetry__Pushgateway__Job: "scrapper"

bot:
build:
context: .
dockerfile: src/LinkTracker.Bot.Api/Dockerfile
network: host
container_name: linktracker-bot
restart: unless-stopped
depends_on:
scrapper:
condition: service_started
kafka-init:
condition: service_completed_successfully
schema-registry:
condition: service_started
pushgateway:
condition: service_started
ports:
- "8091:8091"
- "8092:8092"
- "8011:8011"
env_file:
- src/LinkTracker.Bot.Api/.env
environment:
ASPNETCORE_ENVIRONMENT: "Docker"
Telemetry__Pushgateway__Endpoint: "http://pushgateway:9091/metrics"
Telemetry__Pushgateway__Job: "bot"

bot:
build:
context: .
dockerfile: src/LinkTracker.Bot.Api/Dockerfile
network: host
container_name: linktracker-bot
restart: unless-stopped
depends_on:
scrapper:
condition: service_started
kafka-init:
condition: service_completed_successfully
schema-registry:
condition: service_started
ports:
- "8091:8091"
- "8092:8092"
- "8011:8011"
env_file:
- src/LinkTracker.Bot.Api/.env
environment:
ASPNETCORE_ENVIRONMENT: "Docker"

aiagent:
build:
context: .
Expand Down
9 changes: 0 additions & 9 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -438,13 +438,6 @@ services:
depends_on:
- prometheus

pushgateway:
image: prom/pushgateway:latest
container_name: linktracker-pushgateway
restart: unless-stopped
ports:
- "9091:9091"

prometheus:
image: prom/prometheus:latest
container_name: linktracker-prometheus
Expand All @@ -457,8 +450,6 @@ services:
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
depends_on:
- pushgateway

volumes:
postgres_data:
Expand Down
2 changes: 2 additions & 0 deletions migrations/004_outbox_lease.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE outbox_messages
ADD COLUMN IF NOT EXISTS locked_until TIMESTAMPTZ NULL;
2 changes: 2 additions & 0 deletions migrations/005_drop_filters.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS subscription_filters;
DROP TABLE IF EXISTS filters;
5 changes: 0 additions & 5 deletions monitoring/prometheus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,3 @@ scrape_configs:
- job_name: bot
static_configs:
- targets: ["bot:8011"]

- job_name: pushgateway
honor_labels: true
static_configs:
- targets: [ "pushgateway:9091" ]
32 changes: 16 additions & 16 deletions src/LinkTracker.AiAgent.Infrastructure/Services/GroupingFlushJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,26 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)

private async Task FlushAsync(CancellationToken ct)
{
var flushed = buffer.Flush();
var pending = buffer.Flush()
.SelectMany(entry => grouper
.Group(entry.Updates)
.Select(update => (entry.ChatId, Update: update)))
.OrderByDescending(x => x.Update.Priority)
.ToArray();

foreach (var (chatId, updates) in flushed)
foreach (var (chatId, update) in pending)
{
var grouped = grouper.Group(updates);

foreach (var update in grouped)
try
{
try
{
await publisher.PublishAsync(update, ct);
await publisher.PublishAsync(update, ct);

logger.LogInformation(
"Группа опубликована. ChatId={ChatId}, Count={Count}, Priority={Priority}",
chatId, updates.Count, update.Priority);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogError(ex, "Ошибка публикации сгруппированного обновления. ChatId={ChatId}", chatId);
}
logger.LogInformation(
"Группа опубликована. ChatId={ChatId}, UpdateId={UpdateId}, Priority={Priority}",
chatId, update.Id, update.Priority);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogError(ex, "Ошибка публикации сгруппированного обновления. ChatId={ChatId}", chatId);
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/LinkTracker.Bot.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
using LinkTracker.EnvReader;
using LinkTracker.Shared.Infrastructure.RateLimiting;
using LinkTracker.Shared.Infrastructure.Resilience;
using Prometheus;
using LinkTracker.Shared.Infrastructure.Telemetry;
using Serilog;

var builder = WebApplication.CreateBuilder(args);
Expand Down Expand Up @@ -43,7 +43,7 @@

try
{
app.MapMetrics().RequireHost("*:8011");
app.MapMetricsEndpoint().RequireHost("*:8011");
await app.RunAsync();
}
finally
Expand Down
10 changes: 1 addition & 9 deletions src/LinkTracker.Bot.Api/appsettings.Docker.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,5 @@
"WindowSeconds": 60,
"SegmentsPerWindow": 6,
"QueueLimit": 0
},
"Telemetry": {
"Pushgateway": {
"Enabled": true,
"Endpoint": "http://pushgateway:9091/metrics",
"Job": "bot",
"IntervalMilliseconds": 5000
}
}
}
}
8 changes: 0 additions & 8 deletions src/LinkTracker.Bot.Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,5 @@
"WindowSeconds": 60,
"SegmentsPerWindow": 6,
"QueueLimit": 0
},
"Telemetry": {
"Pushgateway": {
"Enabled": true,
"Endpoint": "http://localhost:9091/metrics",
"Job": "bot",
"IntervalMilliseconds": 5000
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,4 @@ public sealed class AddLinkRequest
public Uri? Link { get; init; }

public IReadOnlyList<string> Tags { get; init; } = [];

public IReadOnlyList<string> Filters { get; init; } = [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,4 @@ public sealed class LinkResponse
public Uri Url { get; init; } = default!;

public IReadOnlyList<string> Tags { get; init; } = [];

public IReadOnlyList<string> Filters { get; init; } = [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ Task<LinkResponse> AddLinkAsync(
long chatId,
Uri link,
IReadOnlyList<string> tags,
IReadOnlyList<string> filters,
CancellationToken ct = default);

Task<LinkResponse> RemoveLinkAsync(long chatId, Uri link, CancellationToken ct = default);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private async Task<DialogNodeResult> ConfirmAsync(DialogContext ctx, Cancellatio

try
{
await scrapperClient.AddLinkAsync(ctx.ChatId, uri, tags, [], ct);
await scrapperClient.AddLinkAsync(ctx.ChatId, uri, tags, ct);

return new DialogNodeResult(
$"Начал отслеживать:\n{pendingUrl}\nТеги: {tagsText}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,19 +104,17 @@ public async Task<LinkResponse> AddLinkAsync(
long chatId,
Uri link,
IReadOnlyList<string> tags,
IReadOnlyList<string> filters,
CancellationToken ct = default)
{
var sw = Stopwatch.StartNew();

try
{
logger.LogInformation(
"Клиент gRPC Scrapper: вызов AddLink. ChatId={ChatId}, Ссылка={Link}, КоличествоТегов={TagsCount}, КоличествоФильтров={FiltersCount}",
"Клиент gRPC Scrapper: вызов AddLink. ChatId={ChatId}, Ссылка={Link}, КоличествоТегов={TagsCount}",
chatId,
link,
tags.Count,
filters.Count);
tags.Count);

try
{
Expand Down Expand Up @@ -180,7 +178,7 @@ public async Task<LinkResponse> RemoveLinkAsync(long chatId, Uri link, Cancellat

private static LinkResponse ToModel(LinkGrpcResponse response)
{
return new LinkResponse { Id = response.Id, Url = new Uri(response.Url), Tags = response.Tags.ToArray(), Filters = [] };
return new LinkResponse { Id = response.Id, Url = new Uri(response.Url), Tags = response.Tags.ToArray() };
}

private static ScrapperClientException ToClientException(RpcException ex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ public Task<LinkResponse> AddLinkAsync(
long chatId,
Uri link,
IReadOnlyList<string> tags,
IReadOnlyList<string> filters,
CancellationToken ct = default)
{
var sw = Stopwatch.StartNew();
Expand All @@ -90,7 +89,7 @@ public Task<LinkResponse> AddLinkAsync(
{
using var request = new HttpRequestMessage(HttpMethod.Post, "/links");
request.Headers.Add("Tg-Chat-Id", chatId.ToString());
request.Content = JsonContent.Create(new AddLinkRequest { Link = link, Tags = tags, Filters = [] });
request.Content = JsonContent.Create(new AddLinkRequest { Link = link, Tags = tags });

using var response = await httpClient.SendAsync(request, token);
await EnsureSuccessStatusCodeAsync(response, token);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ public static IServiceCollection AddTelemetry(
{
services.AddSingleton<IBotMetrics, BotMetrics>();

services.AddOpenTelemetryMetricsWithPushgateway(
configuration,
"bot",
services.AddOpenTelemetryMetrics(
"bot",
BotMetrics.MeterName);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using LinkTracker.Bot.Application.Telemetry.Abstractions;
using LinkTracker.Bot.Application.Updates.Abstractions;
using LinkTracker.Shared.Constants;
using LinkTracker.Shared.Contracts.AiAgent;
using LinkTracker.Shared.Contracts.Bot;
using Telegram.Bot;

Expand All @@ -26,6 +27,16 @@ private static string BuildMessage(LinkUpdate update)
return update.Description[SystemMessageMarkers.FailedLinkReport.Length..];
}

return $"Обновление по ссылке:\n{update.Url}\n\n{update.Description}";
return $"{BuildHeader(update.Priority)}\n{update.Url}\n\n{update.Description}";
}
}

private static string BuildHeader(LinkUpdatePriority priority)
{
return priority switch
{
LinkUpdatePriority.High => "‼️ Важное обновление по ссылке:",
LinkUpdatePriority.Low => "Незначительное обновление по ссылке:",
_ => "Обновление по ссылке:"
};
}
}
1 change: 0 additions & 1 deletion src/LinkTracker.Scrapper.Api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ COPY ["src/LinkTracker.Scrapper.Contracts/LinkTracker.Scrapper.Contracts.csproj"
COPY ["src/LinkTracker.Scrapper.Infrastructure/LinkTracker.Scrapper.Infrastructure.csproj", "src/LinkTracker.Scrapper.Infrastructure/"]
COPY ["src/LinkTracker.Scrapper.Presentation/LinkTracker.Scrapper.Presentation.csproj", "src/LinkTracker.Scrapper.Presentation/"]
COPY ["src/LinkTracker.Scrapper.Storage.Abstractions/LinkTracker.Scrapper.Storage.Abstractions.csproj", "src/LinkTracker.Scrapper.Storage.Abstractions/"]
COPY ["src/LinkTracker.Scrapper.Storage.InMemory/LinkTracker.Scrapper.Storage.InMemory.csproj", "src/LinkTracker.Scrapper.Storage.InMemory/"]
COPY ["src/LinkTracker.Scrapper.Storage.Orm/LinkTracker.Scrapper.Storage.Orm.csproj", "src/LinkTracker.Scrapper.Storage.Orm/"]
COPY ["src/LinkTracker.Scrapper.Storage.Sql/LinkTracker.Scrapper.Storage.Sql.csproj", "src/LinkTracker.Scrapper.Storage.Sql/"]
COPY ["src/LinkTracker.EnvReader/LinkTracker.EnvReader.csproj", "src/LinkTracker.EnvReader/"]
Expand Down
Loading
Loading