Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

public interface TelegramGroupRepository extends CrudRepository<TelegramGroup, Long> {

long countByActiveTrue();

@Query("""
SELECT g FROM TelegramGroup g
WHERE g.active = true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package dev.rubasace.linkedin.games.ldrbot.image;

import dev.rubasace.linkedin.games.ldrbot.metrics.MetricsConstants;
import dev.rubasace.linkedin.games.ldrbot.session.GameDuration;
import dev.rubasace.linkedin.games.ldrbot.session.GameType;
import dev.rubasace.linkedin.games.ldrbot.user.UserInfo;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.bytedeco.opencv.global.opencv_imgcodecs;
import org.bytedeco.opencv.opencv_core.Mat;
import org.slf4j.Logger;
Expand All @@ -20,10 +23,18 @@ public class ImageGameDurationExtractor {

private final ImageGameExtractor imageGameExtractor;
private final ImageDurationExtractor imageDurationExtractor;
private final MeterRegistry meterRegistry;
private final Counter ocrAttemptsCounter;
private final Counter ocrErrorsCounter;

ImageGameDurationExtractor(final ImageGameExtractor imageGameExtractor, final ImageDurationExtractor imageDurationExtractor) {
ImageGameDurationExtractor(final ImageGameExtractor imageGameExtractor,
final ImageDurationExtractor imageDurationExtractor,
final MeterRegistry meterRegistry) {
this.imageGameExtractor = imageGameExtractor;
this.imageDurationExtractor = imageDurationExtractor;
this.meterRegistry = meterRegistry;
this.ocrAttemptsCounter = meterRegistry.counter(MetricsConstants.OCR_ATTEMPTS);
this.ocrErrorsCounter = meterRegistry.counter(MetricsConstants.OCR_ERRORS);
}

public Optional<GameDuration> extractGameDuration(final File imageFile, final Long chatId, final UserInfo userInfo) throws GameDurationExtractionException {
Expand All @@ -32,10 +43,12 @@ public Optional<GameDuration> extractGameDuration(final File imageFile, final Lo
if (gameType.isEmpty()) {
return Optional.empty();
}
ocrAttemptsCounter.increment();
try {
Duration duration = imageDurationExtractor.extractDuration(image, gameType.get().getColors());
return Optional.of(new GameDuration(gameType.get(), duration));
} catch (DurationOCRException e) {
ocrErrorsCounter.increment();
if (e.getCause() != null) {
LOGGER.error(e.getMessage(), e);
} else {
Expand All @@ -45,4 +58,4 @@ public Optional<GameDuration> extractGameDuration(final File imageFile, final Lo
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import dev.rubasace.linkedin.games.ldrbot.chat.CustomTelegramClient;
import dev.rubasace.linkedin.games.ldrbot.chat.UserFeedbackException;
import dev.rubasace.linkedin.games.ldrbot.image.GameDurationExtractionException;
import dev.rubasace.linkedin.games.ldrbot.metrics.MetricsConstants;
import dev.rubasace.linkedin.games.ldrbot.session.GameNameNotFoundException;
import dev.rubasace.linkedin.games.ldrbot.session.SessionAlreadyRegisteredException;
import dev.rubasace.linkedin.games.ldrbot.user.UserNotFoundException;
import dev.rubasace.linkedin.games.ldrbot.util.FormatUtils;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;

@Component
Expand All @@ -25,12 +27,18 @@ class ExceptionHandler {
public static final String GAME_NOT_FOUND_EXCEPTION_MESSAGE = "'%s' is not a valid game name.";

private final CustomTelegramClient customTelegramClient;
private final MeterRegistry meterRegistry;

ExceptionHandler(final CustomTelegramClient customTelegramClient) {
ExceptionHandler(final CustomTelegramClient customTelegramClient, final MeterRegistry meterRegistry) {
this.customTelegramClient = customTelegramClient;
this.meterRegistry = meterRegistry;
}

void notifyUserFeedbackException(final UserFeedbackException userFeedbackException) {
meterRegistry.counter(MetricsConstants.ERRORS,
MetricsConstants.TAG_ERROR_TYPE, userFeedbackException.getClass().getSimpleName())
.increment();

if (userFeedbackException instanceof UnknownCommandException unknownCommandException) {
customTelegramClient.sendErrorMessage(UNKNOWN_COMMAND_MESSAGE_TEMPLATE.formatted(unknownCommandException.getCommand()), unknownCommandException.getChatId());
} else if (userFeedbackException instanceof SessionAlreadyRegisteredException sessionAlreadyRegisteredException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
import dev.rubasace.linkedin.games.ldrbot.configuration.TelegramBotProperties;
import dev.rubasace.linkedin.games.ldrbot.group.GroupNotFoundException;
import dev.rubasace.linkedin.games.ldrbot.image.GameDurationExtractionException;
import dev.rubasace.linkedin.games.ldrbot.metrics.MetricsConstants;
import dev.rubasace.linkedin.games.ldrbot.session.SessionAlreadyRegisteredException;
import dev.rubasace.linkedin.games.ldrbot.util.BackpressureExecutors;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -24,6 +28,7 @@

import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;

@Component
public class MessageController extends AbilityBot implements SpringLongPollingBot {
Expand All @@ -36,17 +41,29 @@ public class MessageController extends AbilityBot implements SpringLongPollingBo
private final String token;
private final ExecutorService controllerExecutor;

private final MeterRegistry meterRegistry;
private final Counter messagesProcessedCounter;
private final Counter unexpectedErrorCounter;
private final AtomicInteger inFlightGauge;

MessageController(final TelegramClient telegramClient,
final MessageService messageService,
final ExceptionHandler exceptionHandler,
final List<AbilityExtension> abilityExtensions,
final TelegramBotProperties telegramBotProperties) {
final TelegramBotProperties telegramBotProperties,
final MeterRegistry meterRegistry) {
super(telegramClient, telegramBotProperties.getUsername(), MapDBContext.onlineInstance("/tmp/" + telegramBotProperties.getUsername()), new BareboneToggle());
this.messageService = messageService;
this.exceptionHandler = exceptionHandler;
this.token = telegramBotProperties.getToken();
this.controllerExecutor = BackpressureExecutors.newBackPressureVirtualThreadPerTaskExecutor("message-controller", MAX_CONSUME_CONCURRENCY);
this.addExtensions(abilityExtensions);

this.meterRegistry = meterRegistry;
this.messagesProcessedCounter = meterRegistry.counter(MetricsConstants.MESSAGES_PROCESSED);
this.unexpectedErrorCounter = meterRegistry.counter(MetricsConstants.ERRORS_UNEXPECTED);
this.inFlightGauge = new AtomicInteger(0);
meterRegistry.gauge(MetricsConstants.MESSAGES_INFLIGHT, inFlightGauge, AtomicInteger::get);
}

@Override
Expand All @@ -56,14 +73,24 @@ public void consume(final List<Update> updates) {

@Override
public void consume(Update update) {
inFlightGauge.incrementAndGet();
Timer.Sample sample = Timer.start(meterRegistry);
try {
doConsume(update);
messagesProcessedCounter.increment();
} catch (Exception e) {
if (e instanceof UserFeedbackException) {
exceptionHandler.notifyUserFeedbackException((UserFeedbackException) e);
if (e instanceof UserFeedbackException userFeedbackException) {
meterRegistry.counter(MetricsConstants.ERRORS,
MetricsConstants.TAG_ERROR_TYPE, e.getClass().getSimpleName())
.increment();
exceptionHandler.notifyUserFeedbackException(userFeedbackException);
} else {
unexpectedErrorCounter.increment();
LOGGER.error("An unexpected error occurred", e);
}
} finally {
sample.stop(meterRegistry.timer(MetricsConstants.MESSAGES_LATENCY));
inFlightGauge.decrementAndGet();
}
}

Expand Down Expand Up @@ -106,4 +133,4 @@ public long creatorId() {
return -1;
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package dev.rubasace.linkedin.games.ldrbot.metrics;

import dev.rubasace.linkedin.games.ldrbot.group.TelegramGroupRepository;
import dev.rubasace.linkedin.games.ldrbot.user.TelegramUserRepository;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;

@Configuration
public class BusinessMetricsConfiguration {

private final TelegramGroupRepository telegramGroupRepository;
private final TelegramUserRepository telegramUserRepository;
private final MeterRegistry meterRegistry;

BusinessMetricsConfiguration(final TelegramGroupRepository telegramGroupRepository,
final TelegramUserRepository telegramUserRepository,
final MeterRegistry meterRegistry) {
this.telegramGroupRepository = telegramGroupRepository;
this.telegramUserRepository = telegramUserRepository;
this.meterRegistry = meterRegistry;
registerGauges();
}

private void registerGauges() {
meterRegistry.gauge(MetricsConstants.GROUPS_ACTIVE, this, cfg -> cfg.telegramGroupRepository.countByActiveTrue());
meterRegistry.gauge(MetricsConstants.USERS_TOTAL, this, cfg -> cfg.telegramUserRepository.count());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package dev.rubasace.linkedin.games.ldrbot.metrics;

import dev.rubasace.linkedin.games.ldrbot.session.GameSessionDeletionEvent;
import dev.rubasace.linkedin.games.ldrbot.session.GameSessionRegistrationEvent;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class GameSessionMetricsListener {

private final MeterRegistry meterRegistry;
private final Counter sessionsDeletedCounter;

GameSessionMetricsListener(final MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.sessionsDeletedCounter = meterRegistry.counter(MetricsConstants.SESSIONS_DELETED);
}

@EventListener
public void onSessionRegistered(GameSessionRegistrationEvent event) {
String gameType = event.getGameInfo() != null ? event.getGameInfo().name() : "unknown";
meterRegistry.counter(MetricsConstants.SESSIONS_REGISTERED,
MetricsConstants.TAG_GAME_TYPE, gameType)
.increment();
}

@EventListener
public void onSessionDeleted(GameSessionDeletionEvent event) {
sessionsDeletedCounter.increment();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package dev.rubasace.linkedin.games.ldrbot.metrics;

public final class MetricsConstants {

private MetricsConstants() {
}

// Message processing
public static final String MESSAGES_PROCESSED = "ldrbot.messages.processed";
public static final String MESSAGES_LATENCY = "ldrbot.messages.latency";
public static final String MESSAGES_INFLIGHT = "ldrbot.messages.inflight";

// OCR
public static final String OCR_ATTEMPTS = "ldrbot.ocr.attempts";
public static final String OCR_ERRORS = "ldrbot.ocr.errors";

// Errors
public static final String ERRORS = "ldrbot.errors";
public static final String ERRORS_UNEXPECTED = "ldrbot.errors.unexpected";

// Background tasks
public static final String BACKGROUND_DURATION = "ldrbot.background.duration";
public static final String BACKGROUND_ERRORS = "ldrbot.background.errors";

// Game sessions
public static final String SESSIONS_REGISTERED = "ldrbot.sessions.registered";
public static final String SESSIONS_DELETED = "ldrbot.sessions.deleted";

// Business gauges
public static final String GROUPS_ACTIVE = "ldrbot.groups.active";
public static final String USERS_TOTAL = "ldrbot.users.total";

// Tags
public static final String TAG_ERROR_TYPE = "error_type";
public static final String TAG_TASK_NAME = "task_name";
public static final String TAG_GAME_TYPE = "game_type";
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
import dev.rubasace.linkedin.games.ldrbot.group.TelegramGroup;
import dev.rubasace.linkedin.games.ldrbot.group.TelegramGroupAdapter;
import dev.rubasace.linkedin.games.ldrbot.group.TelegramGroupService;
import dev.rubasace.linkedin.games.ldrbot.metrics.MetricsConstants;
import dev.rubasace.linkedin.games.ldrbot.util.BackpressureExecutors;
import dev.rubasace.linkedin.games.ldrbot.util.LinkedinTimeUtils;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
Expand All @@ -20,30 +24,49 @@ public class DailyRankingRecalculationService {

private static final Logger LOGGER = LoggerFactory.getLogger(DailyRankingRecalculationService.class);
public static final int MAX_CONCURRENCY = 20;
private static final String TASK_NAME = "ranking";

private final TelegramGroupService telegramGroupService;
private final GroupRankingService groupRankingService;
private final ExecutorService executorService;
private final TelegramGroupAdapter telegramGroupAdapter;
private final MeterRegistry meterRegistry;
private final Timer backgroundDurationTimer;
private final Counter backgroundErrorsCounter;

DailyRankingRecalculationService(final TelegramGroupService telegramGroupService, final GroupRankingService groupRankingService, final TelegramGroupAdapter telegramGroupAdapter) {
DailyRankingRecalculationService(final TelegramGroupService telegramGroupService,
final GroupRankingService groupRankingService,
final TelegramGroupAdapter telegramGroupAdapter,
final MeterRegistry meterRegistry) {
this.telegramGroupService = telegramGroupService;
this.groupRankingService = groupRankingService;
this.telegramGroupAdapter = telegramGroupAdapter;
this.executorService = BackpressureExecutors.newBackPressureVirtualThreadPerTaskExecutor("ranking", MAX_CONCURRENCY);
this.meterRegistry = meterRegistry;
this.backgroundDurationTimer = meterRegistry.timer(MetricsConstants.BACKGROUND_DURATION,
MetricsConstants.TAG_TASK_NAME, TASK_NAME);
this.backgroundErrorsCounter = meterRegistry.counter(MetricsConstants.BACKGROUND_ERRORS,
MetricsConstants.TAG_TASK_NAME, TASK_NAME);
}


public void calculateMissingRankings() {
LocalDate previousGameDay = LinkedinTimeUtils.todayGameDay().minusDays(1);
telegramGroupService.findGroupsWithMissingScores(previousGameDay)
.forEach(telegramGroup -> executorService.execute(() -> generateDailyRanking(telegramGroup, previousGameDay)));
Timer.Sample sample = Timer.start(meterRegistry);
try {
LocalDate previousGameDay = LinkedinTimeUtils.todayGameDay().minusDays(1);
telegramGroupService.findGroupsWithMissingScores(previousGameDay)
.forEach(telegramGroup -> executorService.execute(() -> generateDailyRanking(telegramGroup, previousGameDay)));
} finally {
sample.stop(backgroundDurationTimer);
}
}

private void generateDailyRanking(TelegramGroup telegramGroup, final LocalDate gameDay) {
try {
ChatInfo chatInfo = telegramGroupAdapter.adapt(telegramGroup);
groupRankingService.createDailyRanking(chatInfo, gameDay);
} catch (Exception e) {
backgroundErrorsCounter.increment();
LOGGER.error("Failed to generate daily ranking for group {}, error message: {}", telegramGroup.getChatId(), e.getMessage(), e);
}
}
Expand Down
Loading