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
163 changes: 156 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
![Build](https://github.com/central-university-dev/backend-academy-2025-spring-template/actions/workflows/build.yaml/badge.svg)
# 📌 Link Tracker

# Link Tracker
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-logo-6DB33F?logo=spring)](https://spring.io/projects/spring-boot)
[![Kafka](https://img.shields.io/badge/Apache%20Kafka-logo-000?logo=apachekafka)](https://kafka.apache.org/)
[![Hibernate](https://img.shields.io/badge/Hibernate-logo-59666C?logo=hibernate)](https://hibernate.org/)
[![Docker](https://img.shields.io/badge/Docker-logo-2496ED?logo=docker)](https://www.docker.com/)
[![Testcontainers](https://img.shields.io/badge/Testcontainers-logo-000?logo=testcontainers)](https://testcontainers.com/)
[![Swagger](https://img.shields.io/badge/Swagger-logo-85EA2D?logo=swagger)](https://swagger.io/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-logo-336791?logo=postgresql)](https://www.postgresql.org/)
[![Redis](https://img.shields.io/badge/Redis-logo-DC382D?logo=redis)](https://redis.io/)

----

Чтобы бот заработал нужно в переменные среды загрузить TELEGRAM_TOKEN

----

<!-- этот файл можно и нужно менять -->

Проект сделан в рамках курса Академия Бэкенда.
## 📝 Описание проекта

Приложение для отслеживания обновлений контента по ссылкам.
При появлении новых событий отправляется уведомление в Telegram.
Expand All @@ -21,6 +26,150 @@
* Bot
* Scrapper

Для работы требуется БД `PostgreSQL`. Присутствует опциональная зависимость на `Kafka`.
Для работы требуется `PostgreSQL`, `Redis`, `Kafka`.

### 📟 Схема приложения
![Scrapper](https://github.com/user-attachments/assets/1087e9d5-518f-4650-b470-aaa62a382a9c)


---

## 🤖 Бот

### 📌 Функционал

Бот поддерживает следующие команды:

- [**`/start`**](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/command/helper/StartCommand.java) — регистрация пользователя.
- [**`/help`**](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/command/helper/HelpCommand.java) — список всех доступных команд.
- [**`/track`**](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/command/link/TrackCommand.java) — добавление ссылки для отслеживания
- [**`/untrack`**](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/command/link/UntrackCommand.java) — удаление ссылки из списка отслеживаемых.
- [**`/list`**](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/command/link/ListCommand.java) — получение списка всех отслеживаемых ссылок.
- [**`/tag`**](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/command/tag/TagCommand.java) — выводит все ссылки, у которых есть определенный тег (/tag < tag >)
- [**`/taglist`**](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/command/tag/TagListCommand.java) — все теги, которые ввел пользователь
- [**`/untag`**](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/command/tag/UnTagCommand.java) — удаление тега у ссылки (/untag < tag > < link >)
- [**`/filter`**](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/command/filter/FilterCommand.java) — позволяет добавить фильтр
- [**`/filterlist`**](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/command/filter/FilterListCommand.java) — выводит все фильтры пользователя
- [**`/unfilter`**](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/command/filter/UnFilterCommand.java) — удаление фильтров (/unfilter < filter >)


### 🔄 Взаимодействие со Scrapper

Бот общается со Scrapper API через:
- [**`ScrapperTgChatClient`**](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/client/chat/ScrapperTgChatClientImpl.java) — регистрация и удаление чатов.
- [**`LinkClient`**](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/client/link/ScrapperLinkClientImpl.java) — управление ссылками (добавление, удаление, получение списка ссылок).
- [**`TagClient`**](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/client/tag/ScrapperTagClientImpl.java) — получение списка тегов, получение ссылки по тегу и удаление тега у ссылки.
- [**`ScrapperFilterClient`**](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/client/filter/ScrapperFilterClientImpl.java) — управление фильтрами (добавление, удаление, получение списка фильтров)

### 📩 Получение обновлений

- Бот получает обновления о ссылках через [`UpdateController`](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/api/controller/UpdateController.java) по HTTP либо через [`KafkaUpdateListener`](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/kafka/client/KafkaLinkUpdateListener.java) по Kafka.
- Scrapper отправляет данные по OpenAPI-контракту.
- Обновления рассылаются чатам через [`NotificationService`](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/notification/NotificationService.java).

### 📜 Дополнительно

- Бот поддерживает встроенное [**меню команд**](https://github.com/Delphington/LinterBot/blob/main/bot/src/main/java/backend/academy/bot/processor/UserMessageProcessor.java) в Telegram.

### 🧠 Кеширование Redis

Бот кеширует ответы для следующих команд:
- /tag
- /tag <tag>
- /list

🔄 Кеш автоматически сбрасывается в следующих случаях:
- При добавлении или удалении ссылки (/track, /untrack, /untag)

При вызове команд бот сначала проверяет наличие ответа в кеше. Если данные найдены — используется кеш. В противном случае происходит обращение к Scrapper API, и результат сохраняется в кеш.

---

## 🗂️ Scrapper

### 📌 Функционал

Scrapper обрабатывает запросы от бота:
- **Работа с чатами** через [`ChatController`](https://github.com/Delphington/LinterBot/blob/main/scrapper/src/main/java/backend/academy/scrapper/controller/ChatController.java).
- **Работа с ссылками** через [`LinkController`](https://github.com/Delphington/LinterBot/blob/main/scrapper/src/main/java/backend/academy/scrapper/controller/LinkController.java).
- **Работа с тегами** через [`TagController`](https://github.com/Delphington/LinterBot/blob/main/scrapper/src/main/java/backend/academy/scrapper/controller/TagController.java).
- **Работа с фильтрами** через [`FilterController`](https://github.com/Delphington/LinterBot/blob/main/scrapper/src/main/java/backend/academy/scrapper/controller/FilterController.java).

Все контроллеры работают по OpenAPI-контракту.

### 🔄 Получение обновлений

- [**`LinkUpdateScheduler`**](https://github.com/Delphington/LinterBot/blob/main/scrapper/src/main/java/backend/academy/scrapper/scheduler/LinkUpdaterScheduler.java).
- **📡 Источники данных:**
- GitHub — через [`GitHubClient`](https://github.com/Delphington/LinterBot/blob/main/scrapper/src/main/java/backend/academy/scrapper/tracker/client/GitHubClient.java)
- Stack Overflow — через [`StackOverflowClient`](https://github.com/Delphington/LinterBot/blob/main/scrapper/src/main/java/backend/academy/scrapper/tracker/client/StackOverFlowClient.java)
- **⚙️ Обработка полученных данных**
1. Запрос обновлений
- Для каждого URL запрашивается обновление через открытый API GitHub и StackOverflow
2. Фильтрация обновлений
- Определяются подписанные пользователи (чаты), которые отслеживают данный URL.
- Для каждого пользователя применяется его список фильтров:
3. Парсинг ответа
- Полученный JSON-ответ анализируется, извлекаются нужные значения.
4. Проверка актуальности
- Обновление считается релевантным, если оно произошло после последнего запуска шедулера.
5. Формирование уведомлений
- Отобранные обновления, соответствующие фильтрам, отправляются пользователям либо через HTTP, либо через Kafka.
- **⚙️ Обработка батчей и многопоточность**
- Ссылки на обновления запрашиваются партиями (batch) заданного размера. Каждый батч делится между потоками. Количество потоков настраивается через конфигурацию.

## 📦 Хранение данных

- `tg_chats` — таблица чатов.
- `links` — таблица ссылок.
- `tags` — таблица тегов.
- `filters` — таблица фильтров.
- `filter_list` — таблица фильтров каждого пользователя

### 📌 Схема базы данных

![{E4ED68AF-CD94-4964-B402-74AE70A10960}](https://github.com/user-attachments/assets/26e0773b-61db-41fb-b696-01e68d824b3a)

💡 **Один чат может отслеживать несколько ссылок, а одна ссылка может быть отслеживаемой несколькими чатами.**
📌 **Каждая ссылка может иметь несколько тегов и фильтров в рамках одного чата.**

## 🗂️ Метрики

#### RED
![изображение](https://github.com/user-attachments/assets/e242b8e5-34e5-406f-9ee3-761fd25a7d87)

#### Custom
- Количество пользовательских сообщений в секунду
- График количества активных ссылок в БД по типу (github, stackoverflow)
- p50, p95, p99 времени работы одного scrape по типу (github, stackoverflow)

![image](https://github.com/user-attachments/assets/84ebe7bf-88e4-4563-b14b-5abc35eb3306)

#### jvm
![image](https://github.com/user-attachments/assets/d855023e-8dda-4366-ada0-74e768264bfe)


---


## 🚀 Запуск проекта

1. Клонирование репозитория специальной ветки
```bash
git clone -b feat-docker --single-branch https://github.com/Delphington/LinterBot
```

2. Перейдите в директорию с docker-конфигурацией:
```bash
cd LinterTelegramBot
```

3. Нужно прокинуть специальную переменную среды
```bash
$env:TELEGRAM_TOKEN="YOUR_TOKEN"
```

Для дополнительной справки: [HELP.md](./HELP.md)
4. Запустите сборку и запуск контейнеров:
```bash
docker-compose up --build
```
5 changes: 5 additions & 0 deletions bot/bot.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM eclipse-temurin:23-jdk-alpine
WORKDIR /app
COPY target/bot-1.0.jar /app/bot-1.0.jar
EXPOSE 8080
CMD ["java", "-jar", "bot-1.0.jar"]
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import backend.academy.bot.api.dto.request.LinkUpdate;
import backend.academy.bot.notification.NotificationService;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
Expand Down Expand Up @@ -31,6 +32,8 @@ public void update(@RequestBody @Valid LinkUpdate linkUpdate) {
}

@PostMapping("/public")
@ResponseStatus(HttpStatus.OK)
@Timed(value = "update.timer", histogram = true) // Явное включение гистограммы
public void update() {
log.info("Пришло обновление по ссылке");
}
Expand Down
2 changes: 2 additions & 0 deletions bot/src/main/java/backend/academy/bot/command/Command.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.request.SendMessage;
import io.micrometer.core.annotation.Timed;

public interface Command {

String command();

String description();

@Timed("helpCommandMetric")
SendMessage handle(Update update);

default boolean matchesCommand(Update update) {
Expand Down
19 changes: 19 additions & 0 deletions bot/src/main/java/backend/academy/bot/config/MetricsConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package backend.academy.bot.config;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MetricsConfig {

@Bean
public MeterBinder meterBinder() {
return registry -> {
Counter.builder("msg_count")
.description("Количество сообщений от пользователей")
.register(registry);
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.pengrad.telegrambot.request.SendMessage;
import com.pengrad.telegrambot.request.SetMyCommands;
import com.pengrad.telegrambot.response.BaseResponse;
import io.micrometer.core.instrument.MeterRegistry;
import java.util.List;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
Expand All @@ -25,6 +26,7 @@ public class UserMessageProcessor {
private final TelegramBot telegramBot;
private final List<Command> commandList;
private final UserStateManager userStateManager;
private final MeterRegistry meterRegistry;

public void registerCommands() {
List<BotCommand> commands = commandList.stream()
Expand All @@ -42,6 +44,7 @@ public void registerCommands() {
}

public SendMessage process(Update update) {
meterRegistry.counter("msg_count").increment();
Long id = update.message().chat().id();
userStateManager.createUserIfNotExist(id);

Expand Down
22 changes: 22 additions & 0 deletions bot/src/main/resources/Metrics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# RED

Rate
```
sum by(uri, method) (
rate(http_server_requests_seconds_count{job="Bot"}[1m])
)
```
Errors
```
sum by(uri, status) (
rate(http_server_requests_seconds_count{job="Bot", status=~"4..|5.."}[1m])
)
```
Duration
```
sum by(uri) (
rate(http_server_requests_seconds_sum{job="Bot"}[1m])
)
```

# Custom
51 changes: 38 additions & 13 deletions bot/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
app:
telegram-token: ${TELEGRAM_TOKEN} # env variable
link:
scrapper-uri: "http://localhost:8081"
scrapper-uri: "http://scrapper:8081"
webclient:
timeouts:
connect-timeout: 10s # 10 секунд на установку соединения
Expand Down Expand Up @@ -30,11 +30,11 @@ spring:
type: redis
data:
redis:
host: localhost
host: redis
port: 6379

kafka:
bootstrap-servers: "localhost:29092"
bootstrap-servers: broker:9092
consumer:
auto-offset-reset: earliest
group-id: "consumer-group"
Expand Down Expand Up @@ -131,13 +131,38 @@ bucket4j:
refill-amount: 50 # Количество токенов для пополнения
refill-seconds: 60 # Интервал пополнения в секундах (например, 60 = 1 минута)




#logging:
# structured:
# format:
# file: ecs
# console: ecs
# level:
# root: INFO
management:
server:
port: 8090 # Отдельный порт для метрик
endpoints:
web:
base-path: /
path-mapping:
prometheus: metrics
exposure:
include: "*"

distribution:
percentiles-histogram:
http.server.requests: true # Гистограммы для перцентилей

prometheus:
metrics:
export:
enabled: true # Включить экспорт метрик для Prometheus
web:
server:
auto-time-requests: true # Должен быть здесь!

metrics:
tags:
application: ${spring.application.name} # Добавляет тег с именем приложения


logging:
structured:
format:
file: ecs
console: ecs
level:
root: INFO
Loading
Loading