Compare commits

...

2 Commits

Author SHA1 Message Date
Sheldan
bd00f6d248 [AB-xxx] more instrumenting changes 2025-11-28 20:31:28 +01:00
Sheldan
0d5b7c5855 [AB-xxx] try for instrumenting 2025-11-23 14:54:08 +01:00
32 changed files with 738 additions and 457 deletions

View File

@@ -6,6 +6,8 @@ import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.listener.DefaultListenerResult;
import dev.sheldan.abstracto.core.listener.async.jda.AsyncMessageReceivedListener;
import dev.sheldan.abstracto.core.models.listener.MessageReceivedModel;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.ChannelType;
@@ -19,13 +21,22 @@ public class MassPingMessageListener implements AsyncMessageReceivedListener {
@Autowired
private MassPingService massPingService;
@Autowired
private Tracer tracer;
@Override
public DefaultListenerResult execute(MessageReceivedModel model) {
Message message = model.getMessage();
if(message.getAuthor().isBot() || message.isWebhookMessage() || !message.isFromGuild() || !message.isFromType(ChannelType.TEXT)) {
return DefaultListenerResult.IGNORED;
Span newSpan = tracer.nextSpan().name("mass-ping-filter");
try (Tracer.SpanInScope ws = this.tracer.withSpan(newSpan.start())) {
Message message = model.getMessage();
if (message.getAuthor().isBot() || message.isWebhookMessage() || !message.isFromGuild() || !message.isFromType(ChannelType.TEXT)) {
newSpan.end();
return DefaultListenerResult.IGNORED;
}
massPingService.processMessage(message).whenComplete((unused, throwable) -> {
newSpan.end();
});
}
massPingService.processMessage(message);
return DefaultListenerResult.PROCESSED;
}

View File

@@ -27,6 +27,8 @@ import dev.sheldan.abstracto.experience.service.ExperienceLevelService;
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
@@ -83,11 +85,14 @@ public class Rank extends AbstractConditionableCommand {
@Value("${abstracto.experience.leaderboard.externalUrl}")
private String leaderboardExternalURL;
@Autowired
private Tracer tracer;
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
List<Object> parameters = commandContext.getParameters().getParameters();
Member targetMember = !parameters.isEmpty() ? (Member) parameters.get(0) : commandContext.getAuthor();
if(!targetMember.getGuild().equals(commandContext.getGuild())) {
if (!targetMember.getGuild().equals(commandContext.getGuild())) {
throw new EntityGuildMismatchException();
}
AUserInAServer aUserInAServer = userInServerManagementService.loadOrCreateUser(targetMember);
@@ -97,10 +102,13 @@ public class Rank extends AbstractConditionableCommand {
.builder()
.member(targetMember)
.build();
Span span = tracer.currentSpan();
return future.thenCompose(leaderBoardEntryModel -> {
try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
MessageToSend messageToSend = self.renderMessageToSend(targetMember, rankModel, leaderBoardEntryModel.get(0));
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()));
}).thenApply(result -> CommandResult.fromIgnored());
}
}).thenApply(result -> CommandResult.fromIgnored());
}
@Transactional
@@ -133,7 +141,7 @@ public class Rank extends AbstractConditionableCommand {
@Override
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
Member targetMember;
if(slashCommandParameterService.hasCommandOption(MEMBER_PARAMETER, event)) {
if (slashCommandParameterService.hasCommandOption(MEMBER_PARAMETER, event)) {
targetMember = slashCommandParameterService.getCommandOption(MEMBER_PARAMETER, event, Member.class);
} else {
targetMember = event.getMember();
@@ -145,9 +153,12 @@ public class Rank extends AbstractConditionableCommand {
.builder()
.member(targetMember)
.build();
Span span = tracer.currentSpan();
return future.thenCompose(leaderBoardEntryModel -> {
MessageToSend messageToSend = self.renderMessageToSend(targetMember, rankModel, leaderBoardEntryModel.get(0));
return interactionService.replyMessageToSend(messageToSend, event);
try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
MessageToSend messageToSend = self.renderMessageToSend(targetMember, rankModel, leaderBoardEntryModel.get(0));
return interactionService.replyMessageToSend(messageToSend, event);
}
}).thenApply(result -> CommandResult.fromIgnored());
}

View File

@@ -4,7 +4,8 @@ import dev.sheldan.abstracto.core.service.MemberService;
import dev.sheldan.abstracto.experience.model.LeaderBoard;
import dev.sheldan.abstracto.experience.model.LeaderBoardEntry;
import dev.sheldan.abstracto.experience.model.template.LeaderBoardEntryModel;
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
import org.springframework.beans.factory.annotation.Autowired;
@@ -29,10 +30,7 @@ public class LeaderBoardModelConverter {
private MemberService memberService;
@Autowired
private UserExperienceManagementService userExperienceManagementService;
@Autowired
private LeaderBoardModelConverter self;
private Tracer tracer;
/**
* Converts the complete {@link LeaderBoard leaderBoard} into a list of {@link LeaderBoardEntryModel leaderbaordEntryModels} which contain additional
@@ -62,12 +60,15 @@ public class LeaderBoardModelConverter {
.build();
})
.collect(Collectors.toMap(LeaderBoardEntryModel::getUserId, Function.identity()));
Span span = tracer.currentSpan();
return memberService.getMembersInServerAsync(serverId, userIds).thenApply(members -> {
members.forEach(member -> models.get(member.getIdLong()).setMember(member));
return new ArrayList<>(models.values())
.stream()
.sorted(Comparator.comparing(LeaderBoardEntryModel::getRank)).
collect(Collectors.toList());
try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
members.forEach(member -> models.get(member.getIdLong()).setMember(member));
return new ArrayList<>(models.values())
.stream()
.sorted(Comparator.comparing(LeaderBoardEntryModel::getRank)).
collect(Collectors.toList());
}
});
}
}

View File

@@ -7,6 +7,8 @@ import dev.sheldan.abstracto.core.listener.sync.jda.MessageReceivedListener;
import dev.sheldan.abstracto.core.models.listener.MessageReceivedModel;
import dev.sheldan.abstracto.experience.config.ExperienceFeatureDefinition;
import dev.sheldan.abstracto.experience.service.AUserExperienceService;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import org.springframework.beans.factory.annotation.Autowired;
@@ -23,17 +25,26 @@ public class ExperienceTrackerListener implements AsyncMessageReceivedListener {
@Autowired
private AUserExperienceService userExperienceService;
@Autowired
private Tracer tracer;
@Override
public DefaultListenerResult execute(MessageReceivedModel model) {
Message message = model.getMessage();
if(!message.isFromGuild() || message.isWebhookMessage() || message.getType().isSystem() || message.getAuthor().isBot()) {
return DefaultListenerResult.IGNORED;
}
if(userExperienceService.experienceGainEnabledInChannel(message.getChannel())) {
userExperienceService.addExperience(message.getMember(), model.getMessage());
return DefaultListenerResult.PROCESSED;
} else {
return DefaultListenerResult.IGNORED;
Span newSpan = tracer.nextSpan().name("experience-tracker");
try (Tracer.SpanInScope ws = this.tracer.withSpan(newSpan.start())) {
Message message = model.getMessage();
if(!message.isFromGuild() || message.isWebhookMessage() || message.getType().isSystem() || message.getAuthor().isBot()) {
newSpan.end();
return DefaultListenerResult.IGNORED;
}
if(userExperienceService.experienceGainEnabledInChannel(message.getChannel())) {
userExperienceService.addExperience(message.getMember(), model.getMessage()).whenComplete((unused, throwable) -> {
newSpan.end();
});
return DefaultListenerResult.PROCESSED;
} else {
return DefaultListenerResult.IGNORED;
}
}
}

View File

@@ -1,5 +1,6 @@
package dev.sheldan.abstracto.experience.service;
import dev.sheldan.abstracto.core.metric.service.MetricUtils;
import dev.sheldan.abstracto.core.models.database.*;
import dev.sheldan.abstracto.core.models.template.display.MemberDisplay;
import dev.sheldan.abstracto.core.models.template.display.RoleDisplay;
@@ -25,6 +26,8 @@ import dev.sheldan.abstracto.experience.service.management.DisabledExpRoleManage
import dev.sheldan.abstracto.experience.service.management.ExperienceLevelManagementService;
import dev.sheldan.abstracto.experience.service.management.ExperienceRoleManagementService;
import dev.sheldan.abstracto.experience.service.management.UserExperienceManagementService;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
@@ -114,8 +117,11 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
@Qualifier("experienceUpdateExecutor")
private TaskExecutor experienceUpdateExecutor;
@Autowired
private Tracer tracer;
@Override
public void addExperience(Member member, Message message) {
public CompletableFuture<Void> addExperience(Member member, Message message) {
runTimeExperienceService.takeLock();
try {
Map<Long, Map<Long, Instant>> runtimeExperience = runTimeExperienceService.getRuntimeExperience();
@@ -141,7 +147,7 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
// we store when the user is eligible for experience _again_
Long maxSeconds = configService.getLongValueOrConfigDefault(EXP_COOLDOWN_SECONDS_KEY, serverId);
serverExperience.put(userId, Instant.now().plus(maxSeconds, ChronoUnit.SECONDS));
CompletableFuture.runAsync(() -> self.addExperienceToMember(member, message), experienceUpdateExecutor).exceptionally(throwable -> {
return CompletableFuture.runAsync(() -> self.addExperienceToMember(member, message), MetricUtils.wrapExecutor(experienceUpdateExecutor)).exceptionally(throwable -> {
log.error("Failed to add experience to member {} in server {}.", message.getAuthor().getId(), message.getGuild().getIdLong(), throwable);
return null;
});
@@ -149,6 +155,7 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
} finally {
runTimeExperienceService.releaseLock();
}
return CompletableFuture.completedFuture(null);
}
@@ -297,61 +304,69 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
}
@Transactional
public void addExperienceToMember(Member member, Message message) {
long serverId = member.getGuild().getIdLong();
AServer server = serverManagementService.loadOrCreate(serverId);
List<ADisabledExpRole> disabledExpRoles = disabledExpRoleManagementService.getDisabledRolesForServer(server);
List<ARole> disabledRoles = disabledExpRoles
public CompletableFuture<Void> addExperienceToMember(Member member, Message message) {
CompletableFuture<Void> notificationFuture = CompletableFuture.completedFuture(null);
CompletableFuture<Void> levelActionFuture = CompletableFuture.completedFuture(null);
CompletableFuture<Void> roleFuture = CompletableFuture.completedFuture(null);
Span newSpan = tracer.nextSpan().name("experience-adding");
try (Tracer.SpanInScope ws = this.tracer.withSpan(newSpan.start())) {
long serverId = member.getGuild().getIdLong();
AServer server = serverManagementService.loadOrCreate(serverId);
List<ADisabledExpRole> disabledExpRoles = disabledExpRoleManagementService.getDisabledRolesForServer(server);
List<ARole> disabledRoles = disabledExpRoles
.stream()
.map(ADisabledExpRole::getRole)
.collect(Collectors.toList());
if(roleService.hasAnyOfTheRoles(member, disabledRoles)) {
log.debug("User {} has a experience disable role in server {} - not giving any experience.", member.getIdLong(), serverId);
return;
}
AUserInAServer userInAServer = userInServerManagementService.loadOrCreateUser(member);
Long userInServerId = userInAServer.getUserInServerId();
Optional<AUserExperience> aUserExperienceOptional = userExperienceManagementService.findByUserInServerIdOptional(userInAServer.getUserInServerId());
AUserExperience aUserExperience = aUserExperienceOptional.orElseGet(() -> userExperienceManagementService.createUserInServer(userInAServer));
if(Boolean.FALSE.equals(aUserExperience.getExperienceGainDisabled())) {
List<AExperienceLevel> levels = experienceLevelManagementService.getLevelConfig();
levels.sort(Comparator.comparing(AExperienceLevel::getExperienceNeeded));
if (roleService.hasAnyOfTheRoles(member, disabledRoles)) {
log.debug("User {} has a experience disable role in server {} - not giving any experience.", member.getIdLong(), serverId);
newSpan.end();
return CompletableFuture.completedFuture(null);
}
AUserInAServer userInAServer = userInServerManagementService.loadOrCreateUser(member);
Long userInServerId = userInAServer.getUserInServerId();
Optional<AUserExperience> aUserExperienceOptional = userExperienceManagementService.findByUserInServerIdOptional(userInAServer.getUserInServerId());
AUserExperience aUserExperience = aUserExperienceOptional.orElseGet(() -> userExperienceManagementService.createUserInServer(userInAServer));
if (Boolean.FALSE.equals(aUserExperience.getExperienceGainDisabled())) {
List<AExperienceLevel> levels = experienceLevelManagementService.getLevelConfig();
levels.sort(Comparator.comparing(AExperienceLevel::getExperienceNeeded));
Long minExp = configService.getLongValueOrConfigDefault(ExperienceFeatureConfig.MIN_EXP_KEY, serverId);
Long maxExp = configService.getLongValueOrConfigDefault(ExperienceFeatureConfig.MAX_EXP_KEY, serverId);
Double multiplier = configService.getDoubleValueOrConfigDefault(ExperienceFeatureConfig.EXP_MULTIPLIER_KEY, serverId);
Long experienceRange = maxExp - minExp + 1;
Long gainedExperience = (secureRandom.nextInt(experienceRange.intValue()) + minExp);
gainedExperience = (long) Math.floor(gainedExperience * multiplier);
Long minExp = configService.getLongValueOrConfigDefault(ExperienceFeatureConfig.MIN_EXP_KEY, serverId);
Long maxExp = configService.getLongValueOrConfigDefault(ExperienceFeatureConfig.MAX_EXP_KEY, serverId);
Double multiplier = configService.getDoubleValueOrConfigDefault(ExperienceFeatureConfig.EXP_MULTIPLIER_KEY, serverId);
Long experienceRange = maxExp - minExp + 1;
Long gainedExperience = (secureRandom.nextInt(experienceRange.intValue()) + minExp);
gainedExperience = (long) Math.floor(gainedExperience * multiplier);
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
roles.sort(Comparator.comparing(role -> role.getLevel().getLevel()));
List<AExperienceRole> roles = experienceRoleManagementService.getExperienceRolesForServer(server);
roles.sort(Comparator.comparing(role -> role.getLevel().getLevel()));
log.debug("Handling {}. The user gains {}.", userInServerId, gainedExperience);
log.debug("Handling {}. The user gains {}.", userInServerId, gainedExperience);
Long oldExperience = aUserExperience.getExperience();
Long newExperienceCount = oldExperience + gainedExperience;
aUserExperience.setExperience(newExperienceCount);
AExperienceLevel newLevel = calculateLevel(levels, newExperienceCount);
RoleCalculationResult result = RoleCalculationResult
Long oldExperience = aUserExperience.getExperience();
Long newExperienceCount = oldExperience + gainedExperience;
aUserExperience.setExperience(newExperienceCount);
AExperienceLevel newLevel = calculateLevel(levels, newExperienceCount);
RoleCalculationResult result = RoleCalculationResult
.builder()
.build();
boolean userChangesLevel = !Objects.equals(newLevel.getLevel(), aUserExperience.getCurrentLevel().getLevel());
Integer oldLevel = aUserExperience.getCurrentLevel() != null ? aUserExperience.getCurrentLevel().getLevel() : 0;
if(userChangesLevel) {
log.info("User {} in server {} changed level. New {}, Old {}.", member.getIdLong(),
boolean userChangesLevel = !Objects.equals(newLevel.getLevel(), aUserExperience.getCurrentLevel().getLevel());
Integer oldLevel = aUserExperience.getCurrentLevel() != null ? aUserExperience.getCurrentLevel().getLevel() : 0;
if (userChangesLevel) {
log.info("User {} in server {} changed level. New {}, Old {}.", member.getIdLong(),
member.getGuild().getIdLong(), newLevel.getLevel(),
oldLevel);
aUserExperience.setCurrentLevel(newLevel);
AExperienceRole calculatedNewRole = experienceRoleService.calculateRole(roles, newLevel.getLevel());
Long oldRoleId = aUserExperience.getCurrentExperienceRole() != null && aUserExperience.getCurrentExperienceRole().getRole() != null ? aUserExperience.getCurrentExperienceRole().getRole().getId() : null;
Long newRoleId = calculatedNewRole != null && calculatedNewRole.getRole() != null ? calculatedNewRole.getRole().getId() : null;
result.setOldRoleId(oldRoleId);
result.setNewRoleId(newRoleId);
if(message != null
aUserExperience.setCurrentLevel(newLevel);
AExperienceRole calculatedNewRole = experienceRoleService.calculateRole(roles, newLevel.getLevel());
Long oldRoleId = aUserExperience.getCurrentExperienceRole() != null && aUserExperience.getCurrentExperienceRole().getRole() != null ?
aUserExperience.getCurrentExperienceRole().getRole().getId() : null;
Long newRoleId = calculatedNewRole != null && calculatedNewRole.getRole() != null ? calculatedNewRole.getRole().getId() : null;
result.setOldRoleId(oldRoleId);
result.setNewRoleId(newRoleId);
if (message != null
&& aUserExperience.getLevelUpNotification()
&& featureModeService.featureModeActive(ExperienceFeatureDefinition.EXPERIENCE, serverId, ExperienceFeatureMode.LEVEL_UP_NOTIFICATION)) {
LevelUpNotificationModel model = LevelUpNotificationModel
&&
featureModeService.featureModeActive(ExperienceFeatureDefinition.EXPERIENCE, serverId, ExperienceFeatureMode.LEVEL_UP_NOTIFICATION)) {
LevelUpNotificationModel model = LevelUpNotificationModel
.builder()
.memberDisplay(MemberDisplay.fromMember(member))
.oldExperience(oldExperience)
@@ -361,59 +376,71 @@ public class AUserExperienceServiceBean implements AUserExperienceService {
.newRole(oldRoleId != null ? RoleDisplay.fromRole(oldRoleId) : null)
.newRole(newRoleId != null ? RoleDisplay.fromRole(newRoleId) : null)
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate("experience_level_up_notification", model, serverId);
FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, message.getChannel())).thenAccept(unused -> {
log.info("Sent level up notification to user {} in server {} in channel {}.", member.getIdLong(), serverId, message.getChannel().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to send level up notification to user {} in server {} in channel {}.", member.getIdLong(), serverId, message.getChannel().getIdLong());
return null;
});
}
aUserExperience.setCurrentExperienceRole(calculatedNewRole);
}
aUserExperience.setMessageCount(aUserExperience.getMessageCount() + 1L);
if(userChangesLevel && featureModeService.featureModeActive(ExperienceFeatureDefinition.EXPERIENCE, server, ExperienceFeatureMode.LEVEL_ACTION)) {
levelActionService.applyLevelActionsToUser(aUserExperience, oldLevel)
.thenAccept(unused -> {
log.info("Executed level actions for user {}.", userInServerId);
})
.exceptionally(throwable -> {
log.warn("Failed to execute level actions for user {}.", userInServerId, throwable);
return null;
});
}
if(aUserExperienceOptional.isEmpty()) {
userExperienceManagementService.saveUser(aUserExperience);
}
if(!Objects.equals(result.getOldRoleId(), result.getNewRoleId())) {
if(result.getOldRoleId() != null && result.getNewRoleId() != null) {
roleService.updateRolesIds(member, Arrays.asList(result.getOldRoleId()), Arrays.asList(result.getNewRoleId())).thenAccept(unused -> {
log.debug("Removed role {} from and added role {} to member {} in server {}.", result.getOldRoleId(), result.getNewRoleId(), member.getIdLong(), member.getGuild().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to remove role {} from and add role {} to member {} in server {}.", result.getOldRoleId(), result.getNewRoleId(), member.getIdLong(), member.getGuild().getIdLong(), throwable);
return null;
});
} else {
if(result.getOldRoleId() != null) {
roleService.removeRoleFromMemberAsync(member, result.getOldRoleId()).thenAccept(unused -> {
log.debug("Removed role {} from member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong());
MessageToSend messageToSend = templateService.renderEmbedTemplate("experience_level_up_notification", model, serverId);
FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, message.getChannel())).thenAccept(unused -> {
log.info("Sent level up notification to user {} in server {} in channel {}.", member.getIdLong(), serverId,
message.getChannel().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to remove role {} from member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong(), throwable);
log.warn("Failed to send level up notification to user {} in server {} in channel {}.", member.getIdLong(), serverId,
message.getChannel().getIdLong());
return null;
});
}
if(result.getNewRoleId() != null) {
roleService.addRoleToMemberAsync(member, result.getNewRoleId()).thenAccept(unused -> {
log.debug("Added role {} to member {} in server {}.", result.getNewRoleId(), member.getIdLong(), member.getGuild().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to add role {} to member {} in server {}.", result.getOldRoleId(), member.getIdLong(), member.getGuild().getIdLong(), throwable);
aUserExperience.setCurrentExperienceRole(calculatedNewRole);
}
aUserExperience.setMessageCount(aUserExperience.getMessageCount() + 1L);
if (userChangesLevel &&
featureModeService.featureModeActive(ExperienceFeatureDefinition.EXPERIENCE, server, ExperienceFeatureMode.LEVEL_ACTION)) {
levelActionService.applyLevelActionsToUser(aUserExperience, oldLevel)
.thenAccept(unused -> {
log.info("Executed level actions for user {}.", userInServerId);
})
.exceptionally(throwable -> {
log.warn("Failed to execute level actions for user {}.", userInServerId, throwable);
return null;
});
}
if (aUserExperienceOptional.isEmpty()) {
userExperienceManagementService.saveUser(aUserExperience);
}
if (!Objects.equals(result.getOldRoleId(), result.getNewRoleId())) {
if (result.getOldRoleId() != null && result.getNewRoleId() != null) {
roleService.updateRolesIds(member, Arrays.asList(result.getOldRoleId()), Arrays.asList(result.getNewRoleId())).thenAccept(unused -> {
log.debug("Removed role {} from and added role {} to member {} in server {}.", result.getOldRoleId(), result.getNewRoleId(),
member.getIdLong(), member.getGuild().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to remove role {} from and add role {} to member {} in server {}.", result.getOldRoleId(), result.getNewRoleId(),
member.getIdLong(), member.getGuild().getIdLong(), throwable);
return null;
});
} else {
if (result.getOldRoleId() != null) {
roleService.removeRoleFromMemberAsync(member, result.getOldRoleId()).thenAccept(unused -> {
log.debug("Removed role {} from member {} in server {}.", result.getOldRoleId(), member.getIdLong(),
member.getGuild().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to remove role {} from member {} in server {}.", result.getOldRoleId(), member.getIdLong(),
member.getGuild().getIdLong(), throwable);
return null;
});
}
if (result.getNewRoleId() != null) {
roleService.addRoleToMemberAsync(member, result.getNewRoleId()).thenAccept(unused -> {
log.debug("Added role {} to member {} in server {}.", result.getNewRoleId(), member.getIdLong(), member.getGuild().getIdLong());
}).exceptionally(throwable -> {
log.warn("Failed to add role {} to member {} in server {}.", result.getOldRoleId(), member.getIdLong(),
member.getGuild().getIdLong(), throwable);
return null;
});
}
}
}
} else {
log.debug("Experience gain was disabled. User did not gain any experience.");
}
} else {
log.debug("Experience gain was disabled. User did not gain any experience.");
return CompletableFuture.allOf(notificationFuture, levelActionFuture, roleFuture).whenComplete((unused, throwable) -> {
newSpan.end();
});
}
}

View File

@@ -20,7 +20,7 @@ import java.util.concurrent.CompletableFuture;
*/
public interface AUserExperienceService {
String EXPERIENCE_GAIN_CHANNEL_GROUP_KEY = "experienceGain";
void addExperience(Member member, Message message);
CompletableFuture<Void> addExperience(Member member, Message message);
/**
* Calculates the appropriate level for the given experience amount according to the given {@link AExperienceLevel levels}

View File

@@ -7,6 +7,8 @@ import dev.sheldan.abstracto.core.models.listener.MessageReceivedModel;
import dev.sheldan.abstracto.invitefilter.config.InviteFilterFeatureDefinition;
import dev.sheldan.abstracto.invitefilter.service.InviteLinkFilterService;
import dev.sheldan.abstracto.invitefilter.service.InviteLinkFilterServiceBean;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import org.springframework.beans.factory.annotation.Autowired;
@@ -24,6 +26,9 @@ public class InviteLinkFilterListener implements AsyncMessageReceivedListener {
@Autowired
private InviteLinkFilterServiceBean filterServiceBean;
@Autowired
private Tracer tracer;
@Override
public FeatureDefinition getFeature() {
return InviteFilterFeatureDefinition.INVITE_FILTER;
@@ -31,31 +36,40 @@ public class InviteLinkFilterListener implements AsyncMessageReceivedListener {
@Override
public DefaultListenerResult execute(MessageReceivedModel model) {
Message message = model.getMessage();
Span newSpan = tracer.nextSpan().name("invite-filter");
try (Tracer.SpanInScope ws = this.tracer.withSpan(newSpan.start())) {
Message message = model.getMessage();
if(!message.isFromGuild() || message.isWebhookMessage() || message.getType().isSystem()) {
return DefaultListenerResult.IGNORED;
if (!message.isFromGuild() || message.isWebhookMessage() || message.getType().isSystem()) {
newSpan.end();
return DefaultListenerResult.IGNORED;
}
List<String> foundInvites = inviteLinkFilterService.findInvitesInMessage(message);
if (foundInvites.isEmpty()) {
newSpan.end();
return DefaultListenerResult.IGNORED;
}
if (!inviteLinkFilterService.isInviteFilterActiveInChannel(message.getChannel())) {
newSpan.end();
return DefaultListenerResult.IGNORED;
}
if (inviteLinkFilterService.isMemberImmuneAgainstInviteFilter(message.getMember())) {
log.info("Not checking for invites in message, because author {} in channel {} in guild {} is immune against invite filter.",
message.getMember().getIdLong(), message.getGuild().getIdLong(), message.getChannel().getIdLong());
newSpan.end();
return DefaultListenerResult.IGNORED;
}
// only to reduce code duplication, the interface is too concrete
filterServiceBean.resolveAndCheckInvites(message, foundInvites).whenComplete((unused, throwable) -> {
newSpan.end();
});
return DefaultListenerResult.PROCESSED;
}
List<String> foundInvites = inviteLinkFilterService.findInvitesInMessage(message);
if(foundInvites.isEmpty()){
return DefaultListenerResult.IGNORED;
}
if(!inviteLinkFilterService.isInviteFilterActiveInChannel(message.getChannel())) {
return DefaultListenerResult.IGNORED;
}
if(inviteLinkFilterService.isMemberImmuneAgainstInviteFilter(message.getMember())) {
log.info("Not checking for invites in message, because author {} in channel {} in guild {} is immune against invite filter.",
message.getMember().getIdLong(), message.getGuild().getIdLong(), message.getChannel().getIdLong());
return DefaultListenerResult.IGNORED;
}
// only to reduce code duplication, the interface is too concrete
filterServiceBean.resolveAndCheckInvites(message, foundInvites);
return DefaultListenerResult.PROCESSED;
}
}

View File

@@ -303,12 +303,13 @@ public class InviteLinkFilterServiceBean implements InviteLinkFilterService {
return foundInvites;
}
public void resolveAndCheckInvites(Message message, List<String> foundInvites) {
public CompletableFuture<Void> resolveAndCheckInvites(Message message, List<String> foundInvites) {
List<CompletableFuture<Invite>> inviteList = new ArrayList<>();
JDA jda = message.getJDA();
foundInvites.forEach(s -> inviteList.add(resolveInvite(jda, s)));
CompletableFutureList<Invite> list = new CompletableFutureList<>(inviteList);
CompletableFuture<Void> returningFuture = new CompletableFuture<>();
list.getMainFuture().whenComplete((unused, throwable) -> {
List<Invite> invites = list.getObjects();
Long serverId = message.getGuild().getIdLong();
@@ -353,24 +354,31 @@ public class InviteLinkFilterServiceBean implements InviteLinkFilterService {
}
if(toDelete) {
metricService.incrementCounter(MESSAGE_INVITE_FILTERED);
messageService.deleteMessage(message);
CompletableFuture<Void> deletionFuture = messageService.deleteMessage(message);
CompletableFuture<Void> notificationFuture = CompletableFuture.completedFuture(null);
boolean trackUsages = featureModeService.featureModeActive(InviteFilterFeatureDefinition.INVITE_FILTER, serverId, InviteFilterMode.TRACK_USES);
if(trackUsages) {
targetServers.forEach((targetServerId, serverName) -> storeFilteredInviteLinkUsage(targetServerId, serverName, author));
}
boolean sendNotification = featureModeService.featureModeActive(InviteFilterFeatureDefinition.INVITE_FILTER, serverId, InviteFilterMode.FILTER_NOTIFICATIONS);
if(sendNotification) {
sendDeletionNotification(deletedInvites, message)
notificationFuture = sendDeletionNotification(deletedInvites, message)
.thenAccept(unused1 -> log.info("Sent invite deletion notification.")).exceptionally(throwable1 -> {
log.error("Failed to send invite deletion notification.");
return null;
});
}
CompletableFuture.allOf(deletionFuture, notificationFuture).whenComplete((unused1, throwable1) -> {
returningFuture.complete(null);
});
}
}).exceptionally(throwable -> {
log.error("Invite matching failed.", throwable);
returningFuture.complete(null);
return null;
});
return returningFuture;
}
@PostConstruct

View File

@@ -9,6 +9,8 @@ import dev.sheldan.abstracto.core.service.ProfanityService;
import dev.sheldan.abstracto.profanityfilter.config.ProfanityFilterFeatureDefinition;
import dev.sheldan.abstracto.profanityfilter.service.ProfanityFilterService;
import dev.sheldan.abstracto.profanityfilter.service.ProfanityFilterServiceBean;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import org.springframework.beans.factory.annotation.Autowired;
@@ -29,25 +31,35 @@ public class ProfanityMessageReceivedListener implements AsyncMessageReceivedLis
@Autowired
private ProfanityFilterServiceBean profanityFilterServiceBean;
@Autowired
private Tracer tracer;
@Override
public DefaultListenerResult execute(MessageReceivedModel model) {
Message message = model.getMessage();
if(message.isWebhookMessage() || message.getType().isSystem() || !message.isFromGuild()) {
return DefaultListenerResult.IGNORED;
}
Span newSpan = tracer.nextSpan().name("profanity-filter");
try (Tracer.SpanInScope ws = this.tracer.withSpan(newSpan.start())) {
Message message = model.getMessage();
if (message.isWebhookMessage() || message.getType().isSystem() || !message.isFromGuild()) {
newSpan.end();
return DefaultListenerResult.IGNORED;
}
if(profanityFilterService.isImmuneAgainstProfanityFilter(message.getMember())) {
log.debug("Not checking for profanities in message, because author {} in channel {} in guild {} is immune against profanity filter.",
message.getMember().getIdLong(), message.getGuild().getIdLong(), message.getChannel().getIdLong());
return DefaultListenerResult.IGNORED;
}
if (profanityFilterService.isImmuneAgainstProfanityFilter(message.getMember())) {
log.debug("Not checking for profanities in message, because author {} in channel {} in guild {} is immune against profanity filter.",
message.getMember().getIdLong(), message.getGuild().getIdLong(), message.getChannel().getIdLong());
newSpan.end();
return DefaultListenerResult.IGNORED;
}
Long serverId = model.getServerId();
Optional<ProfanityRegex> potentialProfanityGroup = profanityService.getProfanityRegex(message.getContentRaw(), serverId);
if(potentialProfanityGroup.isPresent()) {
ProfanityRegex foundProfanityGroup = potentialProfanityGroup.get();
profanityFilterServiceBean.handleProfaneMessage(message, foundProfanityGroup);
return DefaultListenerResult.PROCESSED;
Long serverId = model.getServerId();
Optional<ProfanityRegex> potentialProfanityGroup = profanityService.getProfanityRegex(message.getContentRaw(), serverId);
if (potentialProfanityGroup.isPresent()) {
ProfanityRegex foundProfanityGroup = potentialProfanityGroup.get();
profanityFilterServiceBean.handleProfaneMessage(message, foundProfanityGroup).whenComplete((unused, throwable) -> {
newSpan.end();
});
return DefaultListenerResult.PROCESSED;
}
}
return DefaultListenerResult.IGNORED;
}

View File

@@ -238,22 +238,23 @@ public class ProfanityFilterServiceBean implements ProfanityFilterService {
return roleImmunityService.isImmune(member, PROFANITY_FILTER_EFFECT_KEY);
}
public void handleProfaneMessage(Message message, ProfanityRegex foundProfanityGroup) {
public CompletableFuture<Void> handleProfaneMessage(Message message, ProfanityRegex foundProfanityGroup) {
metricService.incrementCounter(PROFANITIES_DETECTED_METRIC);
if(featureModeService.featureModeActive(ProfanityFilterFeatureDefinition.PROFANITY_FILTER, message.getGuild().getIdLong(), ProfanityFilterMode.PROFANITY_REPORT)) {
createProfanityReport(message, foundProfanityGroup).exceptionally(throwable -> {
return createProfanityReport(message, foundProfanityGroup).exceptionally(throwable -> {
log.error("Failed to report or persist profanities in server {} for message {} in channel {}.",
message.getGuild().getIdLong(), message.getChannel().getIdLong(), message.getIdLong(), throwable);
return null;
});
}
if(featureModeService.featureModeActive(ProfanityFilterFeatureDefinition.PROFANITY_FILTER, message.getGuild().getIdLong(), ProfanityFilterMode.AUTO_DELETE_PROFANITIES)) {
messageService.deleteMessage(message).exceptionally(throwable -> {
return messageService.deleteMessage(message).exceptionally(throwable -> {
log.error("Failed to delete profanity message with id {} in channel {} in server {}.",
message.getIdLong(), message.getChannel().getIdLong(), message.getGuild().getIdLong(), throwable);
return null;
});
}
return CompletableFuture.completedFuture(null);
}
@PostConstruct

View File

@@ -8,6 +8,8 @@ import dev.sheldan.abstracto.core.service.GuildService;
import dev.sheldan.abstracto.statistic.config.StatisticFeatureDefinition;
import dev.sheldan.abstracto.statistic.emote.model.database.UsedEmoteType;
import dev.sheldan.abstracto.statistic.emote.service.TrackedEmoteService;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.emoji.CustomEmoji;
import org.springframework.beans.factory.annotation.Autowired;
@@ -30,13 +32,13 @@ public class EmoteTrackingListener implements AsyncMessageReceivedListener {
@Autowired
private GuildService guildService;
@Override
public FeatureDefinition getFeature() {
return StatisticFeatureDefinition.EMOTE_TRACKING;
}
@Autowired
private Tracer tracer;
@Override
public DefaultListenerResult execute(MessageReceivedModel model) {
Span newSpan = tracer.nextSpan().name("experience-tracker");
try (Tracer.SpanInScope ws = this.tracer.withSpan(newSpan.start())) {
Message message = model.getMessage();
if(!message.isFromGuild() || message.isWebhookMessage() || message.getType().isSystem()) {
return DefaultListenerResult.IGNORED;
@@ -50,5 +52,15 @@ public class EmoteTrackingListener implements AsyncMessageReceivedListener {
trackedEmoteService.addEmoteToRuntimeStorage(groupedEmotes.get(0), guildService.getGuildById(model.getServerId()), (long) groupedEmotes.size(), UsedEmoteType.MESSAGE)
);
return DefaultListenerResult.PROCESSED;
} finally {
newSpan.end();
}
}
@Override
public FeatureDefinition getFeature() {
return StatisticFeatureDefinition.EMOTE_TRACKING;
}
}

View File

@@ -207,4 +207,9 @@ public class ListenerExecutorConfig {
return executorService.setupExecutorFor("voiceChatLeftListener");
}
@Bean(name = "genericExecutor")
public TaskExecutor genericExecutor() {
return executorService.setupExecutorFor("genericExecutor");
}
}

View File

@@ -11,6 +11,8 @@ import dev.sheldan.abstracto.core.templating.model.AttachedFile;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.ContextUtils;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
@@ -58,6 +60,9 @@ public class InteractionServiceBean implements InteractionService {
@Autowired
private TemplateService templateService;
@Autowired
private Tracer tracer;
public static final CounterMetric EPHEMERAL_MESSAGES_SEND = CounterMetric
.builder()
.name(DISCORD_API_INTERACTION_METRIC)
@@ -259,84 +264,88 @@ public class InteractionServiceBean implements InteractionService {
}
public CompletableFuture<InteractionHook> replyMessageToSend(MessageToSend messageToSend, IReplyCallback callback) {
ReplyCallbackAction action = null;
if(messageToSend.getUseComponentsV2()) {
action = callback.replyComponents(messageToSend.getComponents()).useComponentsV2();
} else {
if(messageToSend.getMessages() != null && !messageToSend.getMessages().isEmpty()) {
metricService.incrementCounter(MESSAGE_SEND_METRIC);
action = callback.reply(messageToSend.getMessages().get(0));
}
if(messageToSend.getEmbeds() != null && !messageToSend.getEmbeds().isEmpty()) {
if(action != null) {
action = action.addEmbeds(messageToSend.getEmbeds().subList(0, Math.min(10, messageToSend.getEmbeds().size())));
} else {
action = callback.replyEmbeds(messageToSend.getEmbeds());
}
}
if(messageToSend.hasFilesToSend()) {
List<FileUpload> attachedFiles = messageToSend
.getAttachedFiles()
.stream()
.map(AttachedFile::convertToFileUpload)
.collect(Collectors.toList());
if(action != null) {
action.setFiles(attachedFiles);
} else {
Span newSpan = tracer.nextSpan().name("send-message-to-interaction");
try (Tracer.SpanInScope ws = this.tracer.withSpan(newSpan.start())) {
ReplyCallbackAction action = null;
if (messageToSend.getUseComponentsV2()) {
action = callback.replyComponents(messageToSend.getComponents()).useComponentsV2();
} else {
if (messageToSend.getMessages() != null && !messageToSend.getMessages().isEmpty()) {
metricService.incrementCounter(MESSAGE_SEND_METRIC);
action = callback.replyFiles(attachedFiles);
action = callback.reply(messageToSend.getMessages().get(0));
}
}
// this should be last, because we are "faking" a message, by inserting a ., in case there has not been a reply yet
// we could also throw an exception, but we are allowing this to go through
List<ActionRow> actionRows = messageToSend.getActionRows();
if(actionRows != null && !actionRows.isEmpty()) {
if(action == null) {
action = callback.reply(".");
}
action = action.setComponents(actionRows);
AServer server;
if(ContextUtils.isGuildKnown(callback)) {
server = serverManagementService.loadServer(callback.getGuild().getIdLong());
} else {
server = null;
}
actionRows.forEach(components -> components.forEach(component -> {
if(component instanceof ActionComponent) {
String id = ((ActionComponent)component).getId();
MessageToSend.ComponentConfig payload = messageToSend.getComponentPayloads().get(id);
if(payload != null && payload.getPersistCallback()) {
componentPayloadManagementService.createPayload(id, payload.getPayload(), payload.getPayloadType(), payload.getComponentOrigin(), server, payload.getComponentType());
}
if (messageToSend.getEmbeds() != null && !messageToSend.getEmbeds().isEmpty()) {
if (action != null) {
action = action.addEmbeds(messageToSend.getEmbeds().subList(0, Math.min(10, messageToSend.getEmbeds().size())));
} else {
action = callback.replyEmbeds(messageToSend.getEmbeds());
}
}));
}
if (messageToSend.hasFilesToSend()) {
List<FileUpload> attachedFiles = messageToSend
.getAttachedFiles()
.stream()
.map(AttachedFile::convertToFileUpload)
.collect(Collectors.toList());
if (action != null) {
action.setFiles(attachedFiles);
} else {
metricService.incrementCounter(MESSAGE_SEND_METRIC);
action = callback.replyFiles(attachedFiles);
}
}
// this should be last, because we are "faking" a message, by inserting a ., in case there has not been a reply yet
// we could also throw an exception, but we are allowing this to go through
List<ActionRow> actionRows = messageToSend.getActionRows();
if (actionRows != null && !actionRows.isEmpty()) {
if (action == null) {
action = callback.reply(".");
}
action = action.setComponents(actionRows);
AServer server;
if (ContextUtils.isGuildKnown(callback)) {
server = serverManagementService.loadServer(callback.getGuild().getIdLong());
} else {
server = null;
}
actionRows.forEach(components -> components.forEach(component -> {
if (component instanceof ActionComponent) {
String id = ((ActionComponent) component).getId();
MessageToSend.ComponentConfig payload = messageToSend.getComponentPayloads().get(id);
if (payload != null && payload.getPersistCallback()) {
componentPayloadManagementService.createPayload(id, payload.getPayload(), payload.getPayloadType(),
payload.getComponentOrigin(), server, payload.getComponentType());
}
}
}));
}
}
}
if(messageToSend.getEphemeral()) {
if(ContextUtils.hasGuild(callback)) {
log.info("Sending ephemeral message to interaction in guild {} in channel {} for user {}.",
if (messageToSend.getEphemeral()) {
if (ContextUtils.hasGuild(callback)) {
log.info("Sending ephemeral message to interaction in guild {} in channel {} for user {}.",
callback.getGuild().getIdLong(), callback.getChannel().getId(),
callback.getUser().getIdLong());
} else {
log.info("Sending ephemeral message to user {}.", callback.getUser().getIdLong());
} else {
log.info("Sending ephemeral message to user {}.", callback.getUser().getIdLong());
}
metricService.incrementCounter(EPHEMERAL_MESSAGES_SEND);
if (action != null) {
action = action.setEphemeral(messageToSend.getEphemeral());
}
}
metricService.incrementCounter(EPHEMERAL_MESSAGES_SEND);
if(action != null) {
action = action.setEphemeral(messageToSend.getEphemeral());
if (ContextUtils.isGuildKnown(callback)) {
Set<Message.MentionType> allowedMentions = allowedMentionService.getAllowedMentionsFor(callback.getMessageChannel(), messageToSend);
if (action != null) {
action = action.setAllowedMentions(allowedMentions);
}
}
}
if(ContextUtils.isGuildKnown(callback)) {
Set<Message.MentionType> allowedMentions = allowedMentionService.getAllowedMentionsFor(callback.getMessageChannel(), messageToSend);
if (action != null) {
action = action.setAllowedMentions(allowedMentions);
}
}
if(action == null) {
throw new AbstractoRunTimeException("The callback did not result in any message.");
if (action == null) {
throw new AbstractoRunTimeException("The callback did not result in any message.");
}
return action.submit();
}
return action.submit();
}
@Override

View File

@@ -4,6 +4,7 @@ import com.google.gson.Gson;
import dev.sheldan.abstracto.core.config.FeatureConfig;
import dev.sheldan.abstracto.core.interaction.InteractionResult;
import dev.sheldan.abstracto.core.interaction.button.ButtonPostInteractionExecution;
import dev.sheldan.abstracto.core.metric.service.MetricUtils;
import dev.sheldan.abstracto.core.models.database.ComponentPayload;
import dev.sheldan.abstracto.core.interaction.button.ButtonPayload;
import dev.sheldan.abstracto.core.service.FeatureConfigService;
@@ -63,7 +64,7 @@ public class SyncButtonClickedListenerBean extends ListenerAdapter {
@Override
public void onButtonInteraction(@Nonnull ButtonInteractionEvent event) {
if(listenerList == null) return;
CompletableFuture.runAsync(() -> self.executeListenerLogic(event), buttonClickedExecutor).exceptionally(throwable -> {
CompletableFuture.runAsync(() -> self.executeListenerLogic(event), MetricUtils.wrapExecutor(buttonClickedExecutor)).exceptionally(throwable -> {
log.error("Failed to execute listener logic in async button event.", throwable);
return null;
});

View File

@@ -7,6 +7,7 @@ import dev.sheldan.abstracto.core.interaction.context.message.listener.MessageCo
import dev.sheldan.abstracto.core.metric.service.CounterMetric;
import dev.sheldan.abstracto.core.metric.service.MetricService;
import dev.sheldan.abstracto.core.metric.service.MetricTag;
import dev.sheldan.abstracto.core.metric.service.MetricUtils;
import dev.sheldan.abstracto.core.models.listener.interaction.MessageContextInteractionModel;
import dev.sheldan.abstracto.core.service.FeatureConfigService;
import dev.sheldan.abstracto.core.service.FeatureFlagService;
@@ -65,7 +66,7 @@ public class MessageContextCommandListenerBean extends ListenerAdapter {
@Override
public void onMessageContextInteraction(MessageContextInteractionEvent event) {
if(listenerList == null) return;
CompletableFuture.runAsync(() -> self.executeListenerLogic(event), messageContextCommandExecutor).exceptionally(throwable -> {
CompletableFuture.runAsync(() -> self.executeListenerLogic(event), MetricUtils.wrapExecutor(messageContextCommandExecutor)).exceptionally(throwable -> {
log.error("Failed to execute listener logic in async message context event.", throwable);
return null;
});

View File

@@ -3,9 +3,9 @@ package dev.sheldan.abstracto.core.interaction.menu;
import com.google.gson.Gson;
import dev.sheldan.abstracto.core.config.FeatureConfig;
import dev.sheldan.abstracto.core.interaction.ComponentPayloadManagementService;
import dev.sheldan.abstracto.core.interaction.InteractionResult;
import dev.sheldan.abstracto.core.interaction.menu.listener.StringSelectMenuListener;
import dev.sheldan.abstracto.core.interaction.menu.listener.StringSelectMenuListenerModel;
import dev.sheldan.abstracto.core.metric.service.MetricUtils;
import dev.sheldan.abstracto.core.models.database.ComponentPayload;
import dev.sheldan.abstracto.core.service.FeatureConfigService;
import dev.sheldan.abstracto.core.service.FeatureFlagService;
@@ -60,7 +60,7 @@ public class StringSelectMenuListenerBean extends ListenerAdapter {
public void onStringSelectInteraction(@Nonnull StringSelectInteractionEvent event) {
if(listenerList == null) return;
event.deferEdit().queue();
CompletableFuture.runAsync(() -> self.executeListenerLogic(event), buttonClickedExecutor).exceptionally(throwable -> {
CompletableFuture.runAsync(() -> self.executeListenerLogic(event), MetricUtils.wrapExecutor(buttonClickedExecutor)).exceptionally(throwable -> {
log.error("Failed to execute listener logic in async button event.", throwable);
return null;
});

View File

@@ -6,6 +6,7 @@ import dev.sheldan.abstracto.core.interaction.ComponentPayloadManagementService;
import dev.sheldan.abstracto.core.interaction.InteractionResult;
import dev.sheldan.abstracto.core.interaction.modal.ModalPayload;
import dev.sheldan.abstracto.core.interaction.modal.ModalPostInteractionExecution;
import dev.sheldan.abstracto.core.metric.service.MetricUtils;
import dev.sheldan.abstracto.core.models.database.ComponentPayload;
import dev.sheldan.abstracto.core.service.FeatureConfigService;
import dev.sheldan.abstracto.core.service.FeatureFlagService;
@@ -62,7 +63,7 @@ public class ModalInteractionListenerBean extends ListenerAdapter {
@Override
public void onModalInteraction(@Nonnull ModalInteractionEvent event) {
if(listenerList == null) return;
CompletableFuture.runAsync(() -> self.executeListenerLogic(event), modalInteractionExecutor).exceptionally(throwable -> {
CompletableFuture.runAsync(() -> self.executeListenerLogic(event), MetricUtils.wrapExecutor(modalInteractionExecutor)).exceptionally(throwable -> {
log.error("Failed to execute listener logic in modal interaction event.", throwable);
return null;
});

View File

@@ -19,18 +19,31 @@ import dev.sheldan.abstracto.core.interaction.slash.payload.SlashCommandConfirma
import dev.sheldan.abstracto.core.metric.service.CounterMetric;
import dev.sheldan.abstracto.core.metric.service.MetricService;
import dev.sheldan.abstracto.core.metric.service.MetricTag;
import dev.sheldan.abstracto.core.metric.service.MetricUtils;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.service.ConfigService;
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.utils.ContextUtils;
import dev.sheldan.abstracto.scheduling.model.JobParameters;
import dev.sheldan.abstracto.scheduling.service.SchedulerService;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import dev.sheldan.abstracto.core.utils.ContextUtils;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.PostConstruct;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
@@ -45,15 +58,6 @@ import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Nonnull;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@Component
@Slf4j
public class SlashCommandListenerBean extends ListenerAdapter {
@@ -100,6 +104,12 @@ public class SlashCommandListenerBean extends ListenerAdapter {
@Autowired
private SchedulerService schedulerService;
@Autowired
private ObservationRegistry observationRegistry;
@Autowired
private Tracer tracer;
@Autowired
private ComponentPayloadManagementService componentPayloadManagementService;
@@ -107,48 +117,66 @@ public class SlashCommandListenerBean extends ListenerAdapter {
public static final String SLASH_COMMAND_CONFIRMATION_ORIGIN = "SLASH_COMMAND_CONFIRMATION";
public static final CounterMetric SLASH_COMMANDS_PROCESSED_COUNTER = CounterMetric
.builder()
.name(CommandReceivedHandler.COMMAND_PROCESSED)
.tagList(Arrays.asList(MetricTag.getTag(CommandReceivedHandler.STATUS_TAG, "processed"), MetricTag.getTag(CommandReceivedHandler.TYPE_TAG, "slash")))
.build();
.builder()
.name(CommandReceivedHandler.COMMAND_PROCESSED)
.tagList(Arrays.asList(MetricTag.getTag(CommandReceivedHandler.STATUS_TAG, "processed"), MetricTag.getTag(CommandReceivedHandler.TYPE_TAG, "slash")))
.build();
public List<Command> getSlashCommands() {
if(commands == null || commands.isEmpty()) {
if (commands == null || commands.isEmpty()) {
return new ArrayList<>();
}
return commands.stream()
.filter(command -> command.getConfiguration()
.getSlashCommandConfig().isEnabled())
.collect(Collectors.toList());
.filter(command -> command.getConfiguration()
.getSlashCommandConfig().isEnabled())
.collect(Collectors.toList());
}
@Override
public void onSlashCommandInteraction(SlashCommandInteractionEvent event) {
try {
if(commands == null || commands.isEmpty()) return;
if(ContextUtils.hasGuild(event.getInteraction())) {
log.debug("Executing slash command in guild {} from user {}.", event.getGuild().getIdLong(), event.getUser().getIdLong());
} else {
log.debug("Executing slash command by user {}", event.getUser().getIdLong());
Observation observation = Observation.createNotStarted("slash-command-received", this.observationRegistry);
observation.observe(() -> {
try {
if (commands == null || commands.isEmpty()) {
return;
}
if (ContextUtils.hasGuild(event.getInteraction())) {
log.debug("Executing slash command in guild {} from user {}.", event.getGuild().getIdLong(), event.getUser().getIdLong());
} else {
log.debug("Executing slash command by user {}", event.getUser().getIdLong());
}
Span span = tracer.currentSpan();
CompletableFuture.runAsync(() -> {
try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
self.executeListenerLogic(event).whenComplete((unused, throwable) -> {
span.end();
});
}
}, slashCommandExecutor).exceptionally(throwable -> {
log.error("Failed to execute listener logic in async slash command event.", throwable);
return null;
});
} catch (Exception exception) {
log.error("Failed to process slash command interaction event.", exception);
}
CompletableFuture.runAsync(() -> self.executeListenerLogic(event), slashCommandExecutor).exceptionally(throwable -> {
log.error("Failed to execute listener logic in async slash command event.", throwable);
return null;
});
} catch (Exception exception) {
log.error("Failed to process slash command interaction event.", exception);
}
});
}
@Transactional
public void executeListenerLogic(SlashCommandInteractionEvent event) {
public CompletableFuture<Void> executeListenerLogic(SlashCommandInteractionEvent event) {
Optional<Command> potentialCommand = findCommand(event);
potentialCommand.ifPresent(command -> {
if (potentialCommand.isPresent()) {
Command command = potentialCommand.get();
metricService.incrementCounter(SLASH_COMMANDS_PROCESSED_COUNTER);
try {
commandService.isCommandExecutable(command, event).thenAccept(conditionResult -> {
self.executeCommand(event, command, conditionResult);
Span span = tracer.currentSpan();
span.tag("command-name", command.getConfiguration().getName());
return commandService.isCommandExecutable(command, event).thenCompose(conditionResult -> {
try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
return self.executeCommand(event, command, conditionResult);
}
}).exceptionally(throwable -> {
log.error("Error while executing command {}", command.getConfiguration().getName(), throwable);
CommandResult commandResult = CommandResult.fromError(throwable.getMessage(), throwable);
@@ -159,18 +187,24 @@ public class SlashCommandListenerBean extends ListenerAdapter {
log.error("Error while checking if command {} is executable.", command.getConfiguration().getName(), exception);
CommandResult commandResult = CommandResult.fromError(exception.getMessage(), exception);
self.executePostCommandListener(command, event, commandResult);
return CompletableFuture.completedFuture(null);
}
});
} else {
return CompletableFuture.completedFuture(null);
}
}
@Override
public void onCommandAutoCompleteInteraction(@Nonnull CommandAutoCompleteInteractionEvent event) {
try {
if(commands == null || commands.isEmpty()) return;
CompletableFuture.runAsync(() -> self.executeAutCompleteListenerLogic(event), slashCommandAutoCompleteExecutor).exceptionally(throwable -> {
log.error("Failed to execute listener logic in async auto complete interaction event.", throwable);
return null;
});
if (commands == null || commands.isEmpty()) {
return;
}
CompletableFuture.runAsync(() -> self.executeAutCompleteListenerLogic(event), MetricUtils.wrapExecutor(slashCommandAutoCompleteExecutor))
.exceptionally(throwable -> {
log.error("Failed to execute listener logic in async auto complete interaction event.", throwable);
return null;
});
} catch (Exception e) {
log.error("Failed to process slash command auto complete interaction.", e);
}
@@ -183,15 +217,17 @@ public class SlashCommandListenerBean extends ListenerAdapter {
try {
List<String> fullRepliesList = command.performAutoComplete(event);
List<String> replies = fullRepliesList.subList(0, Math.min(fullRepliesList.size(), OptionData.MAX_CHOICES));
event.replyChoiceStrings(replies).queue(unused -> {},
throwable -> {
if(ContextUtils.hasGuild(event)) {
log.error("Failed to respond to complete of command {} in guild {} by user {}.",
command.getConfiguration().getName(), event.getGuild().getIdLong(), event.getUser().getIdLong());
} else {
log.error("Failed to resp ond to complete of command {} for user {}.", command.getConfiguration().getName(), event.getUser().getIdLong());
}
});
event.replyChoiceStrings(replies).queue(unused -> {
},
throwable -> {
if (ContextUtils.hasGuild(event)) {
log.error("Failed to respond to complete of command {} in guild {} by user {}.",
command.getConfiguration().getName(), event.getGuild().getIdLong(), event.getUser().getIdLong());
} else {
log.error("Failed to resp ond to complete of command {} for user {}.", command.getConfiguration().getName(),
event.getUser().getIdLong());
}
});
} catch (Exception exception) {
log.error("Error while executing autocomplete of command {}.", command.getConfiguration().getName(), exception);
}
@@ -199,8 +235,9 @@ public class SlashCommandListenerBean extends ListenerAdapter {
}
@Transactional
public void continueSlashCommand(Long interactionId, ButtonInteractionEvent buttonInteractionEvent, SlashCommandConfirmationPayload slashCommandConfirmationPayload) {
if(COMMANDS_WAITING_FOR_CONFIRMATION.containsKey(interactionId)) {
public void continueSlashCommand(Long interactionId, ButtonInteractionEvent buttonInteractionEvent,
SlashCommandConfirmationPayload slashCommandConfirmationPayload) {
if (COMMANDS_WAITING_FOR_CONFIRMATION.containsKey(interactionId)) {
DriedSlashCommand driedSlashCommand = COMMANDS_WAITING_FOR_CONFIRMATION.get(interactionId);
Command commandInstance = driedSlashCommand.getCommand();
String commandName = commandInstance.getConfiguration().getName();
@@ -221,23 +258,28 @@ public class SlashCommandListenerBean extends ListenerAdapter {
return null;
});
} else {
log.warn("Interaction was not found in internal map - not continuing interaction from user {} in server {}.", buttonInteractionEvent.getUser().getIdLong(), buttonInteractionEvent.getGuild().getIdLong());
log.warn("Interaction was not found in internal map - not continuing interaction from user {} in server {}.",
buttonInteractionEvent.getUser().getIdLong(), buttonInteractionEvent.getGuild().getIdLong());
}
}
@Transactional
public void cleanupSlashCommandConfirmation(SlashCommandConfirmationPayload slashCommandConfirmationPayload, ButtonInteractionEvent buttonInteractionEvent) {
log.debug("Cleaning up component {} and {}.", slashCommandConfirmationPayload.getConfirmationPayloadId(), slashCommandConfirmationPayload.getAbortPayloadId());
componentPayloadManagementService.deletePayloads(Arrays.asList(slashCommandConfirmationPayload.getAbortPayloadId(), slashCommandConfirmationPayload.getConfirmationPayloadId()));
public void cleanupSlashCommandConfirmation(SlashCommandConfirmationPayload slashCommandConfirmationPayload,
ButtonInteractionEvent buttonInteractionEvent) {
log.debug("Cleaning up component {} and {}.", slashCommandConfirmationPayload.getConfirmationPayloadId(),
slashCommandConfirmationPayload.getAbortPayloadId());
componentPayloadManagementService.deletePayloads(
Arrays.asList(slashCommandConfirmationPayload.getAbortPayloadId(), slashCommandConfirmationPayload.getConfirmationPayloadId()));
}
@Transactional
public void removeSlashCommandConfirmationInteraction(Long interactionId, String confirmationPayload, String abortPayload) {
if(COMMANDS_WAITING_FOR_CONFIRMATION.containsKey(interactionId)) {
if (COMMANDS_WAITING_FOR_CONFIRMATION.containsKey(interactionId)) {
DriedSlashCommand removedSlashCommand = COMMANDS_WAITING_FOR_CONFIRMATION.remove(interactionId);
SlashCommandInteractionEvent event = removedSlashCommand.getEvent();
event.getInteraction().getHook().deleteOriginal().queue();
log.info("Remove interaction for command {} in server {} from user {}.", removedSlashCommand.getCommand().getConfiguration().getName(), event.getGuild().getIdLong(), event.getUser().getIdLong());
log.info("Remove interaction for command {} in server {} from user {}.", removedSlashCommand.getCommand().getConfiguration().getName(),
event.getGuild().getIdLong(), event.getUser().getIdLong());
} else {
log.info("Did not find interaction to clean up.");
}
@@ -245,9 +287,9 @@ public class SlashCommandListenerBean extends ListenerAdapter {
}
@Transactional(rollbackFor = AbstractoRunTimeException.class)
public void executeCommand(SlashCommandInteractionEvent event, Command command, ConditionResult conditionResult) {
public CompletableFuture<Void> executeCommand(SlashCommandInteractionEvent event, Command command, ConditionResult conditionResult) {
String commandName = command.getConfiguration().getName();
if(command.getConfiguration().isRequiresConfirmation() && conditionResult.isResult()) {
if (command.getConfiguration().isRequiresConfirmation() && conditionResult.isResult()) {
DriedSlashCommand slashCommand = DriedSlashCommand
.builder()
.command(command)
@@ -281,19 +323,22 @@ public class SlashCommandListenerBean extends ListenerAdapter {
.commandName(commandName)
.build();
Long userId = event.getUser().getIdLong();
interactionService.replyEmbed(COMMAND_CONFIRMATION_MESSAGE_TEMPLATE_KEY, model, event).thenAccept(interactionHook -> {
log.info("Sent confirmation for command {} in server {} for user {}.", commandName, serverId, userId);
}).exceptionally(throwable -> {
log.warn("Failed to send confirmation for command {} in server {} for user {}.", commandName, serverId, userId);
return null;
});
CompletableFuture<Void> replyFuture =
interactionService.replyEmbed(COMMAND_CONFIRMATION_MESSAGE_TEMPLATE_KEY, model, event).thenAccept(interactionHook -> {
log.info("Sent confirmation for command {} in server {} for user {}.", commandName, serverId, userId);
}).exceptionally(throwable -> {
log.warn("Failed to send confirmation for command {} in server {} for user {}.", commandName, serverId, userId);
return null;
});
scheduleConfirmationDeletion(event.getIdLong(), confirmationId, abortId, serverId);
return replyFuture;
} else {
CompletableFuture<CommandResult> commandOutput;
if(conditionResult.isResult()) {
if (conditionResult.isResult()) {
commandOutput = command.executeSlash(event).thenApply(commandResult -> {
if(ContextUtils.hasGuild(event)) {
log.info("Command {} in server {} was executed by user {}.", command.getConfiguration().getName(), event.getGuild().getIdLong(), event.getUser().getIdLong());
if (ContextUtils.hasGuild(event)) {
log.info("Command {} in server {} was executed by user {}.", command.getConfiguration().getName(), event.getGuild().getIdLong(),
event.getUser().getIdLong());
} else {
log.info("Command {} was executed by user {}.", command.getConfiguration().getName(), event.getUser().getId());
}
@@ -302,7 +347,7 @@ public class SlashCommandListenerBean extends ListenerAdapter {
} else {
commandOutput = CompletableFuture.completedFuture(CommandResult.fromCondition(conditionResult));
}
commandOutput.thenAccept(commandResult -> {
return commandOutput.thenAccept(commandResult -> {
self.executePostCommandListener(command, event, commandResult);
}).exceptionally(throwable -> {
log.error("Error while handling post execution of command {}", commandName, throwable);
@@ -337,26 +382,26 @@ public class SlashCommandListenerBean extends ListenerAdapter {
private Optional<Command> findCommand(SlashCommandInteractionEvent event) {
return commands
.stream()
.filter(command -> command.getConfiguration().getSlashCommandConfig().isEnabled())
.filter(command -> command.getConfiguration().getSlashCommandConfig().matchesInteraction(event.getInteraction()))
.findAny();
.stream()
.filter(command -> command.getConfiguration().getSlashCommandConfig().isEnabled())
.filter(command -> command.getConfiguration().getSlashCommandConfig().matchesInteraction(event.getInteraction()))
.findAny();
}
private Optional<Command> findCommand(CommandAutoCompleteInteractionEvent event) {
return commands
.stream()
.filter(command -> command.getConfiguration().getSlashCommandConfig().isEnabled())
.filter(command -> command.getConfiguration().getSlashCommandConfig().matchesInteraction(event.getInteraction()))
.findAny();
.stream()
.filter(command -> command.getConfiguration().getSlashCommandConfig().isEnabled())
.filter(command -> command.getConfiguration().getSlashCommandConfig().matchesInteraction(event.getInteraction()))
.findAny();
}
@PostConstruct
public void filterPostProcessors() {
metricService.registerCounter(SLASH_COMMANDS_PROCESSED_COUNTER, "Slash Commands processed");
executions = executions
.stream()
.filter(PostCommandExecution::supportsSlash)
.collect(Collectors.toList());
.stream()
.filter(PostCommandExecution::supportsSlash)
.collect(Collectors.toList());
}
}

View File

@@ -6,6 +6,9 @@ import dev.sheldan.abstracto.core.interactive.setup.callback.MessageInteractionC
import dev.sheldan.abstracto.core.listener.DefaultListenerResult;
import dev.sheldan.abstracto.core.listener.async.jda.AsyncMessageReceivedListener;
import dev.sheldan.abstracto.core.models.listener.MessageReceivedModel;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
@@ -20,6 +23,9 @@ public class InteractiveMessageReceivedListener implements AsyncMessageReceivedL
// TODO timeout
private Map<Long, Map<Long, Map<Long, MessageInteractionCallback>>> callbacks = new HashMap<>();
@Autowired
private Tracer tracer;
private static final Lock runTimeLock = new ReentrantLock();
@Override
@@ -40,7 +46,8 @@ public class InteractiveMessageReceivedListener implements AsyncMessageReceivedL
public boolean executeCallback(MessageReceivedModel model) {
runTimeLock.lock();
try {
Span newSpan = tracer.nextSpan().name("interactive-message-tracker");
try (Tracer.SpanInScope ws = this.tracer.withSpan(newSpan.start())){
if(callbacks.containsKey(model.getServerId())) {
Map<Long, Map<Long, MessageInteractionCallback>> channelMap = callbacks.get(model.getServerId());
if(channelMap.containsKey(model.getMessage().getChannel().getIdLong())) {
@@ -56,6 +63,7 @@ public class InteractiveMessageReceivedListener implements AsyncMessageReceivedL
}
}
} finally {
newSpan.end();
runTimeLock.unlock();
}
return false;

View File

@@ -1,12 +1,10 @@
package dev.sheldan.abstracto.core.listener.async.jda;
import dev.sheldan.abstracto.core.command.service.ExceptionService;
import dev.sheldan.abstracto.core.listener.ListenerService;
import dev.sheldan.abstracto.core.models.listener.MessageReceivedModel;
import dev.sheldan.abstracto.core.service.BotService;
import dev.sheldan.abstracto.core.service.FeatureConfigService;
import dev.sheldan.abstracto.core.service.FeatureFlagService;
import dev.sheldan.abstracto.core.service.MessageCache;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
@@ -28,18 +26,6 @@ public class AsyncMessageReceivedListenerBean extends ListenerAdapter {
@Autowired(required = false)
private List<AsyncMessageReceivedListener> listenerList;
@Autowired
private FeatureConfigService featureConfigService;
@Autowired
private FeatureFlagService featureFlagService;
@Autowired
private BotService botService;
@Autowired
private ExceptionService exceptionService;
@Autowired
@Qualifier("messageReceivedExecutor")
private TaskExecutor messageReceivedExecutor;
@@ -47,14 +33,21 @@ public class AsyncMessageReceivedListenerBean extends ListenerAdapter {
@Autowired
private ListenerService listenerService;
@Autowired
private ObservationRegistry observationRegistry;
@Override
@Transactional
public void onMessageReceived(@Nonnull MessageReceivedEvent event) {
if(listenerList == null) return;
if(!event.isFromGuild()) return;
messageCache.putMessageInCache(event.getMessage());
MessageReceivedModel model = getModel(event);
listenerList.forEach(leaveListener -> listenerService.executeFeatureAwareListener(leaveListener, model, messageReceivedExecutor));
Observation observation = Observation.createNotStarted("async-message-received", this.observationRegistry);
observation.lowCardinalityKeyValue("some-tag", "some-value");
observation.observe(() -> {
messageCache.putMessageInCache(event.getMessage());
MessageReceivedModel model = getModel(event);
listenerList.forEach(leaveListener -> listenerService.executeFeatureAwareListener(leaveListener, model, messageReceivedExecutor));
});
}
private MessageReceivedModel getModel(MessageReceivedEvent event) {

View File

@@ -1,6 +1,7 @@
package dev.sheldan.abstracto.core.listener.async.jda;
import dev.sheldan.abstracto.core.command.service.ExceptionService;
import dev.sheldan.abstracto.core.metric.service.MetricUtils;
import dev.sheldan.abstracto.core.models.cache.CachedMessage;
import dev.sheldan.abstracto.core.service.BotService;
import dev.sheldan.abstracto.core.service.CacheEntityService;
@@ -54,7 +55,7 @@ public class AsyncPrivateMessageReceivedListenerBean extends ListenerAdapter {
cacheEntityService.buildCachedMessageFromMessage(event.getMessage()).thenAccept(cachedMessage ->
privateMessageReceivedListeners.forEach(messageReceivedListener -> CompletableFuture.runAsync(() ->
self.executeIndividualPrivateMessageReceivedListener(cachedMessage, messageReceivedListener, event)
, privateMessageReceivedExecutor)
, MetricUtils.wrapExecutor(privateMessageReceivedExecutor))
.exceptionally(throwable -> {
log.error("Async private message receiver listener {} failed.", messageReceivedListener, throwable);
return null;

View File

@@ -1,16 +1,15 @@
package dev.sheldan.abstracto.core.listener.sync.jda;
import dev.sheldan.abstracto.core.command.service.ExceptionService;
import dev.sheldan.abstracto.core.listener.ListenerService;
import dev.sheldan.abstracto.core.metric.service.CounterMetric;
import dev.sheldan.abstracto.core.metric.service.MetricService;
import dev.sheldan.abstracto.core.metric.service.MetricTag;
import dev.sheldan.abstracto.core.models.listener.MessageReceivedModel;
import dev.sheldan.abstracto.core.service.BotService;
import dev.sheldan.abstracto.core.service.FeatureConfigService;
import dev.sheldan.abstracto.core.service.FeatureFlagService;
import dev.sheldan.abstracto.core.service.MessageCache;
import dev.sheldan.abstracto.core.utils.BeanUtils;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.annotation.Observed;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
@@ -25,6 +24,7 @@ import java.util.List;
@Component
@Slf4j
@Observed
public class MessageReceivedListenerBean extends ListenerAdapter {
@Autowired
@@ -33,24 +33,15 @@ public class MessageReceivedListenerBean extends ListenerAdapter {
@Autowired(required = false)
private List<MessageReceivedListener> listenerList;
@Autowired
private FeatureConfigService featureConfigService;
@Autowired
private FeatureFlagService featureFlagService;
@Autowired
private BotService botService;
@Autowired
private ExceptionService exceptionService;
@Autowired
private ListenerService listenerService;
@Autowired
private MetricService metricService;
@Autowired
private ObservationRegistry observationRegistry;
public static final String MESSAGE_METRIC = "message";
public static final String ACTION = "action";
private static final CounterMetric MESSAGE_RECEIVED_COUNTER = CounterMetric
@@ -63,12 +54,14 @@ public class MessageReceivedListenerBean extends ListenerAdapter {
@Transactional
public void onMessageReceived(@Nonnull MessageReceivedEvent event) {
if(!event.isFromGuild()) return;
metricService.incrementCounter(MESSAGE_RECEIVED_COUNTER);
messageCache.putMessageInCache(event.getMessage());
if(listenerList == null) return;
MessageReceivedModel model = getModel(event);
listenerList.forEach(messageReceivedListener -> listenerService.executeFeatureAwareListener(messageReceivedListener, model));
Observation observation = Observation.createNotStarted("message-received", this.observationRegistry);
observation.observe(() -> {
metricService.incrementCounter(MESSAGE_RECEIVED_COUNTER);
messageCache.putMessageInCache(event.getMessage());
if (listenerList == null) return;
MessageReceivedModel model = getModel(event);
listenerList.forEach(messageReceivedListener -> listenerService.executeFeatureAwareListener(messageReceivedListener, model));
});
}
private MessageReceivedModel getModel(MessageReceivedEvent event) {

View File

@@ -3,6 +3,10 @@ package dev.sheldan.abstracto.core.service;
import dev.sheldan.abstracto.core.logging.OkHttpLogger;
import dev.sheldan.abstracto.core.metric.OkHttpMetrics;
import dev.sheldan.abstracto.core.models.SystemInfo;
import io.micrometer.context.ContextExecutorService;
import io.micrometer.context.ContextSnapshotFactory;
import io.micrometer.core.instrument.binder.okhttp3.OkHttpObservationInterceptor;
import io.micrometer.observation.ObservationRegistry;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
@@ -11,9 +15,13 @@ import net.dv8tion.jda.api.utils.ChunkingFilter;
import net.dv8tion.jda.api.utils.MemberCachePolicy;
import net.dv8tion.jda.api.utils.cache.CacheFlag;
import net.dv8tion.jda.internal.utils.IOUtil;
import okhttp3.Dispatcher;
import okhttp3.OkHttpClient;
import okhttp3.internal.Util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.task.TaskExecutor;
import org.springframework.stereotype.Service;
import java.lang.management.ManagementFactory;
@@ -24,6 +32,9 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
@@ -46,6 +57,18 @@ public class BotServiceBean implements BotService {
@Value("${abstracto.memberCachePolicy:ALL}")
private String memberCachePolicy;
@Autowired
@Qualifier("genericExecutor")
private TaskExecutor genericExecutor;
@Autowired
private ObservationRegistry observationRegistry;
private OkHttpObservationInterceptor.Builder defaultInterceptorBuilder() {
return OkHttpObservationInterceptor.builder(observationRegistry, "okhttp.requests")
.uriMapper(req -> req.url().encodedPath());
}
private static final Map<String, MemberCachePolicy> POSSIBLE_MEMBER_CACHE_POLICIES = new HashMap<>();
static {
@@ -84,6 +107,10 @@ public class BotServiceBean implements BotService {
OkHttpClient.Builder defaultBuilder = IOUtil.newHttpClientBuilder();
defaultBuilder.addInterceptor(okHttpMetrics);
defaultBuilder.addInterceptor(okHttpLogger);
ThreadPoolExecutor executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
defaultBuilder.dispatcher(new Dispatcher(ContextExecutorService.wrap(executor, ContextSnapshotFactory.builder().build()::captureAll)));
defaultBuilder.addInterceptor(defaultInterceptorBuilder().build());
builder.setHttpClientBuilder(defaultBuilder);
this.instance = builder.build();

View File

@@ -14,6 +14,8 @@ import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.CompletableFutureList;
import dev.sheldan.abstracto.core.utils.FileService;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
@@ -35,6 +37,7 @@ import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@@ -77,6 +80,9 @@ public class ChannelServiceBean implements ChannelService {
@Autowired
private ServerManagementService serverManagementService;
@Autowired
private Tracer tracer;
public static final CounterMetric CHANNEL_CREATE_METRIC = CounterMetric
.builder()
.name(DISCORD_API_INTERACTION_METRIC)
@@ -209,114 +215,119 @@ public class ChannelServiceBean implements ChannelService {
@Override
public List<CompletableFuture<Message>> sendMessageToSendToChannel(MessageToSend messageToSend, MessageChannel textChannel) {
messageToSend.setEphemeral(false);
if(textChannel instanceof GuildMessageChannel guildMessageChannel) {
long maxFileSize = guildMessageChannel.getGuild().getMaxFileSize();
// in this case, we cannot upload the file, so we need to fail
messageToSend.getAttachedFiles().forEach(attachedFile -> {
if(attachedFile.getFile().length() > maxFileSize) {
throw new UploadFileTooLargeException(attachedFile.getFile().length(), maxFileSize);
}
});
}
List<CompletableFuture<Message>> futures = new ArrayList<>();
List<MessageCreateAction> allMessageActions = new ArrayList<>();
if(messageToSend.getUseComponentsV2() && messageToSend.getComponents() != null && !messageToSend.getComponents().isEmpty()) {
metricService.incrementCounter(MESSAGE_SEND_METRIC);
allMessageActions.add(textChannel.sendMessageComponents(messageToSend.getComponents()).useComponentsV2());
} else {
Iterator<MessageEmbed> embedIterator = messageToSend.getEmbeds().iterator();
for (int i = 0; i < messageToSend.getMessages().size(); i++) {
Span newSpan = tracer.nextSpan().name("send-message");
try (Tracer.SpanInScope ws = this.tracer.withSpan(newSpan.start())) {
newSpan.tag("channel.id", textChannel.getIdLong());
messageToSend.setEphemeral(false);
if (textChannel instanceof GuildMessageChannel guildMessageChannel) {
long maxFileSize = guildMessageChannel.getGuild().getMaxFileSize();
// in this case, we cannot upload the file, so we need to fail
messageToSend.getAttachedFiles().forEach(attachedFile -> {
if (attachedFile.getFile().length() > maxFileSize) {
throw new UploadFileTooLargeException(attachedFile.getFile().length(), maxFileSize);
}
});
}
List<CompletableFuture<Message>> futures = new ArrayList<>();
List<MessageCreateAction> allMessageActions = new ArrayList<>();
if (messageToSend.getUseComponentsV2() && messageToSend.getComponents() != null && !messageToSend.getComponents().isEmpty()) {
metricService.incrementCounter(MESSAGE_SEND_METRIC);
String text = messageToSend.getMessages().get(i);
allMessageActions.add(textChannel.sendMessageComponents(messageToSend.getComponents()).useComponentsV2());
} else {
Iterator<MessageEmbed> embedIterator = messageToSend.getEmbeds().iterator();
for (int i = 0; i < messageToSend.getMessages().size(); i++) {
metricService.incrementCounter(MESSAGE_SEND_METRIC);
String text = messageToSend.getMessages().get(i);
List<MessageEmbed> messageEmbeds = new ArrayList<>();
while (embedIterator.hasNext()) {
MessageEmbed embedToAdd = embedIterator.next();
if ((currentEmbedLength(messageEmbeds) + embedToAdd.getLength()) >= MessageEmbed.EMBED_MAX_LENGTH_BOT) {
break;
}
messageEmbeds.add(embedToAdd);
embedIterator.remove();
}
MessageCreateAction messageAction = textChannel.sendMessage(text);
if (!messageEmbeds.isEmpty()) {
messageAction.setEmbeds(messageEmbeds);
}
allMessageActions.add(messageAction);
}
List<MessageEmbed> messageEmbeds = new ArrayList<>();
while(embedIterator.hasNext()) {
// reset the iterator, because if the if in the above while iterator loop applied, we already took it out from the iterator
// but we didnt add it yet, so it would be lost
embedIterator = messageToSend.getEmbeds().iterator();
while (embedIterator.hasNext()) {
MessageEmbed embedToAdd = embedIterator.next();
if((currentEmbedLength(messageEmbeds) + embedToAdd.getLength()) >= MessageEmbed.EMBED_MAX_LENGTH_BOT) {
break;
if ((currentEmbedLength(messageEmbeds) + embedToAdd.getLength()) >= MessageEmbed.EMBED_MAX_LENGTH_BOT && !messageEmbeds.isEmpty()) {
allMessageActions.add(textChannel.sendMessageEmbeds(messageEmbeds));
metricService.incrementCounter(MESSAGE_SEND_METRIC);
messageEmbeds = new ArrayList<>();
}
messageEmbeds.add(embedToAdd);
embedIterator.remove();
}
MessageCreateAction messageAction = textChannel.sendMessage(text);
if(!messageEmbeds.isEmpty()) {
messageAction.setEmbeds(messageEmbeds);
}
allMessageActions.add(messageAction);
}
List<MessageEmbed> messageEmbeds = new ArrayList<>();
// reset the iterator, because if the if in the above while iterator loop applied, we already took it out from the iterator
// but we didnt add it yet, so it would be lost
embedIterator = messageToSend.getEmbeds().iterator();
while(embedIterator.hasNext()) {
MessageEmbed embedToAdd = embedIterator.next();
if((currentEmbedLength(messageEmbeds) + embedToAdd.getLength()) >= MessageEmbed.EMBED_MAX_LENGTH_BOT && !messageEmbeds.isEmpty()) {
if (!messageEmbeds.isEmpty()) {
allMessageActions.add(textChannel.sendMessageEmbeds(messageEmbeds));
metricService.incrementCounter(MESSAGE_SEND_METRIC);
messageEmbeds = new ArrayList<>();
}
messageEmbeds.add(embedToAdd);
}
if(!messageEmbeds.isEmpty()) {
allMessageActions.add(textChannel.sendMessageEmbeds(messageEmbeds));
metricService.incrementCounter(MESSAGE_SEND_METRIC);
}
List<ActionRow> actionRows = messageToSend.getActionRows();
if(!actionRows.isEmpty()) {
List<List<ActionRow>> groupedActionRows = ListUtils.partition(actionRows, ComponentService.MAX_BUTTONS_PER_ROW);
for (int i = 0; i < allMessageActions.size(); i++) {
allMessageActions.set(i, allMessageActions.get(i).setComponents(groupedActionRows.get(i)));
}
for (int i = allMessageActions.size(); i < groupedActionRows.size(); i++) {
// TODO maybe possible nicer
allMessageActions.add(textChannel.sendMessage(".").setComponents(groupedActionRows.get(i)));
}
AServer server = null;
if(textChannel instanceof GuildChannel) {
GuildChannel channel = (GuildChannel) textChannel;
server = serverManagementService.loadServer(channel.getGuild());
}
for (ActionRow row : actionRows) {
for (ActionRowChildComponent component : row) {
if (component instanceof ActionComponent) {
String id = ((ActionComponent) component).getId();
MessageToSend.ComponentConfig payload = messageToSend.getComponentPayloads().get(id);
if (payload != null && payload.getPersistCallback()) {
componentPayloadManagementService.createPayload(id, payload.getPayload(), payload.getPayloadType(), payload.getComponentOrigin(), server, payload.getComponentType());
List<ActionRow> actionRows = messageToSend.getActionRows();
if (!actionRows.isEmpty()) {
List<List<ActionRow>> groupedActionRows = ListUtils.partition(actionRows, ComponentService.MAX_BUTTONS_PER_ROW);
for (int i = 0; i < allMessageActions.size(); i++) {
allMessageActions.set(i, allMessageActions.get(i).setComponents(groupedActionRows.get(i)));
}
for (int i = allMessageActions.size(); i < groupedActionRows.size(); i++) {
// TODO maybe possible nicer
allMessageActions.add(textChannel.sendMessage(".").setComponents(groupedActionRows.get(i)));
}
AServer server = null;
if (textChannel instanceof GuildChannel) {
GuildChannel channel = (GuildChannel) textChannel;
server = serverManagementService.loadServer(channel.getGuild());
}
for (ActionRow row : actionRows) {
for (ActionRowChildComponent component : row) {
if (component instanceof ActionComponent) {
String id = ((ActionComponent) component).getId();
MessageToSend.ComponentConfig payload = messageToSend.getComponentPayloads().get(id);
if (payload != null && payload.getPersistCallback()) {
componentPayloadManagementService.createPayload(id, payload.getPayload(), payload.getPayloadType(),
payload.getComponentOrigin(), server, payload.getComponentType());
}
}
}
}
}
}
if(messageToSend.hasFilesToSend()) {
List<FileUpload> attachedFiles = messageToSend
.getAttachedFiles()
.stream()
.map(AttachedFile::convertToFileUpload)
.collect(Collectors.toList());
if(!allMessageActions.isEmpty()) {
// in case there has not been a message, we need to increment it
allMessageActions.set(0, allMessageActions.get(0).addFiles(attachedFiles));
} else {
metricService.incrementCounter(MESSAGE_SEND_METRIC);
allMessageActions.add(textChannel.sendFiles(attachedFiles));
if (messageToSend.hasFilesToSend()) {
List<FileUpload> attachedFiles = messageToSend
.getAttachedFiles()
.stream()
.map(AttachedFile::convertToFileUpload)
.collect(Collectors.toList());
if (!allMessageActions.isEmpty()) {
// in case there has not been a message, we need to increment it
allMessageActions.set(0, allMessageActions.get(0).addFiles(attachedFiles));
} else {
metricService.incrementCounter(MESSAGE_SEND_METRIC);
allMessageActions.add(textChannel.sendFiles(attachedFiles));
}
}
Set<Message.MentionType> allowedMentions = allowedMentionService.getAllowedMentionsFor(textChannel, messageToSend);
allMessageActions.forEach(messageAction -> {
if (messageToSend.getReferencedMessageId() != null) {
messageAction = messageAction.setMessageReference(messageToSend.getReferencedMessageId());
if (messageToSend.getMessageConfig() != null && !messageToSend.getMessageConfig().getMentionsReferencedMessage()) {
messageAction = messageAction.mentionRepliedUser(false);
}
}
futures.add(messageAction.setAllowedMentions(allowedMentions).submit());
});
}
return futures;
}
Set<Message.MentionType> allowedMentions = allowedMentionService.getAllowedMentionsFor(textChannel, messageToSend);
allMessageActions.forEach(messageAction -> {
if(messageToSend.getReferencedMessageId() != null) {
messageAction = messageAction.setMessageReference(messageToSend.getReferencedMessageId());
if(messageToSend.getMessageConfig() != null && !messageToSend.getMessageConfig().getMentionsReferencedMessage()) {
messageAction = messageAction.mentionRepliedUser(false);
}
}
futures.add(messageAction.setAllowedMentions(allowedMentions).submit());
});
return futures;
}
private Integer currentEmbedLength(List<MessageEmbed> messageEmbeds) {

View File

@@ -2,6 +2,7 @@ package dev.sheldan.abstracto.core.service;
import dev.sheldan.abstracto.core.config.FeatureConfig;
import dev.sheldan.abstracto.core.listener.*;
import dev.sheldan.abstracto.core.metric.service.MetricUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.TaskExecutor;
@@ -84,7 +85,7 @@ public class ListenerServiceBean implements ListenerService {
return;
}
try {
CompletableFuture.runAsync(() -> self.executeFeatureListenerInTransaction(listener, model), executor).exceptionally(throwable -> {
CompletableFuture.runAsync(() -> self.executeFeatureListenerInTransaction(listener, model), MetricUtils.wrapExecutor(executor)).exceptionally(throwable -> {
log.error("Feature aware async Listener {} failed with async exception:", listener.getClass().getName(), throwable);
return null;
});
@@ -111,7 +112,7 @@ public class ListenerServiceBean implements ListenerService {
@Override
public <T extends ListenerModel, R extends ListenerExecutionResult> void executeListener(AbstractoListener<T, R> listener, T model, TaskExecutor executor) {
try {
CompletableFuture.runAsync(() -> self.executeListenerInTransaction(listener, model), executor).exceptionally(throwable -> {
CompletableFuture.runAsync(() -> self.executeListenerInTransaction(listener, model), MetricUtils.wrapExecutor(executor)).exceptionally(throwable -> {
log.error("Async Listener {} failed with async exception:", listener.getClass().getName(), throwable);
return null;
});

View File

@@ -4,6 +4,7 @@ import dev.sheldan.abstracto.core.command.model.database.ACommand;
import dev.sheldan.abstracto.core.command.service.management.CommandInServerManagementService;
import dev.sheldan.abstracto.core.command.service.management.CommandManagementService;
import dev.sheldan.abstracto.core.listener.AsyncStartupListener;
import dev.sheldan.abstracto.core.metric.service.MetricUtils;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AChannelType;
import dev.sheldan.abstracto.core.models.database.ARole;
@@ -21,6 +22,8 @@ import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.apache.commons.collections4.SetUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.task.TaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -65,6 +68,10 @@ public class StartupServiceBean implements Startup {
@Autowired
private ProfanityService profanityService;
@Autowired
@Qualifier("genericExecutor")
private TaskExecutor genericExecutor;
@Override
public void startBot() throws LoginException {
service.login();
@@ -95,7 +102,7 @@ public class StartupServiceBean implements Startup {
} catch (Exception e) {
log.error("Startup listener {} failed.", asyncStartupListener, e);
}
}).thenAccept(unused -> log.info("Startup listener {} finished.", asyncStartupListener))
}, MetricUtils.wrapExecutor(genericExecutor)).thenAccept(unused -> log.info("Startup listener {} finished.", asyncStartupListener))
.exceptionally(throwable -> {
log.error("Startup listener {} failed.", asyncStartupListener, throwable);
return null;

View File

@@ -2,9 +2,10 @@ package dev.sheldan.abstracto.core.templating.loading;
import dev.sheldan.abstracto.core.config.ServerContext;
import dev.sheldan.abstracto.core.templating.model.EffectiveTemplate;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.templating.service.management.EffectiveTemplateManagementService;
import freemarker.cache.TemplateLoader;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -26,6 +27,9 @@ public class DatabaseTemplateLoader implements TemplateLoader {
@Autowired
private ServerContext serverContext;
@Autowired
private Tracer tracer;
/**
* Loads the content of the template object
* @param s The key of the template to load
@@ -34,6 +38,9 @@ public class DatabaseTemplateLoader implements TemplateLoader {
@Override
public Object findTemplateSource(String s) throws IOException {
Optional<EffectiveTemplate> templateByKey;
Span newSpan = tracer.nextSpan().name("load-template");
try (Tracer.SpanInScope ws = this.tracer.withSpan(newSpan.start())) {
newSpan.tag("template.key", s);
if(s.contains("/")) {
String[] parts = s.split("/");
templateByKey = effectiveTemplateManagementService.getTemplateByKeyAndServer(parts[1], Long.parseLong(parts[0]));
@@ -41,6 +48,9 @@ public class DatabaseTemplateLoader implements TemplateLoader {
templateByKey = effectiveTemplateManagementService.getTemplateByKey(s);
}
return templateByKey.orElse(null);
} finally {
newSpan.end();
}
}
@Override

View File

@@ -32,6 +32,8 @@ import dev.sheldan.abstracto.core.utils.FileService;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@@ -97,6 +99,10 @@ public class TemplateServiceBean implements TemplateService {
@Autowired
private FileService fileService;
@Autowired
private Tracer tracer;
/**
* Formats the passed count with the embed used for formatting pages.
*

View File

@@ -48,5 +48,10 @@
<groupId>com.github.everit-org.json-schema</groupId>
<artifactId>org.everit.json.schema</artifactId>
</dependency>
<dependency>
<groupId>dev.sheldan.abstracto.core</groupId>
<artifactId>metrics-int</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,14 @@
package dev.sheldan.abstracto.core.metric.config;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.aop.ObservedAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ObservationConfig {
@Bean
ObservedAspect observedAspect(ObservationRegistry registry) {
return new ObservedAspect(registry);
}
}

View File

@@ -9,5 +9,25 @@
<artifactId>metrics-int</artifactId>
<dependencies>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>net.ttddyy.observation</groupId>
<artifactId>datasource-micrometer-spring-boot</artifactId>
<version>1.0.5</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.core.metric.service;
import io.micrometer.context.ContextSnapshotFactory;
import java.util.concurrent.Executor;
public class MetricUtils {
public static Executor wrapExecutor(Executor e) {
return ContextSnapshotFactory
.builder()
.build()
.captureAll()
.wrapExecutor(e);
}
}