Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.togetherjava.tjbot.db.Database;
import org.togetherjava.tjbot.features.Features;
import org.togetherjava.tjbot.features.SlashCommandAdapter;
import org.togetherjava.tjbot.features.analytics.Metrics;
import org.togetherjava.tjbot.features.system.BotCore;
import org.togetherjava.tjbot.logging.LogMarkers;
import org.togetherjava.tjbot.logging.discord.DiscordLogging;
Expand Down Expand Up @@ -82,13 +83,15 @@ public static void runBot(Config config) {
}
Database database = new Database("jdbc:sqlite:" + databasePath.toAbsolutePath());

Metrics metrics = new Metrics(database);

JDA jda = JDABuilder.createDefault(config.getToken())
.enableIntents(GatewayIntent.GUILD_MEMBERS, GatewayIntent.MESSAGE_CONTENT)
.build();

jda.awaitReady();

BotCore core = new BotCore(jda, database, config);
BotCore core = new BotCore(jda, database, config, metrics);
CommandReloading.reloadCommands(jda, core);
core.scheduleRoutines(jda);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.togetherjava.tjbot.config.FeatureBlacklist;
import org.togetherjava.tjbot.config.FeatureBlacklistConfig;
import org.togetherjava.tjbot.db.Database;
import org.togetherjava.tjbot.features.analytics.Metrics;
import org.togetherjava.tjbot.features.basic.MemberCountDisplayRoutine;
import org.togetherjava.tjbot.features.basic.PingCommand;
import org.togetherjava.tjbot.features.basic.QuoteBoardForwarder;
Expand Down Expand Up @@ -91,7 +92,7 @@
* it with the system.
* <p>
* To add a new slash command, extend the commands returned by
* {@link #createFeatures(JDA, Database, Config)}.
* {@link #createFeatures(JDA, Database, Config, Metrics)}.
*/
public class Features {
private Features() {
Expand All @@ -107,9 +108,12 @@ private Features() {
* @param jda the JDA instance commands will be registered at
* @param database the database of the application, which features can use to persist data
* @param config the configuration features should use
* @param metrics the metrics service for tracking analytics
* @return a collection of all features
*/
public static Collection<Feature> createFeatures(JDA jda, Database database, Config config) {
@SuppressWarnings("unused")
public static Collection<Feature> createFeatures(JDA jda, Database database, Config config,
Metrics metrics) {
FeatureBlacklistConfig blacklistConfig = config.getFeatureBlacklistConfig();
JShellEval jshellEval = new JShellEval(config.getJshell(), config.getGitHubApiKey());

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.togetherjava.tjbot.features.analytics;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.togetherjava.tjbot.db.Database;
import org.togetherjava.tjbot.db.generated.tables.MetricEvents;

import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* Service for tracking and recording events for analytics purposes.
*/
public final class Metrics {
private static final Logger logger = LoggerFactory.getLogger(Metrics.class);

private final Database database;

private final ExecutorService service = Executors.newSingleThreadExecutor();

/**
* Creates a new instance.
*
* @param database the database to use for storing and retrieving analytics data
*/
public Metrics(Database database) {
this.database = database;
}

/**
* Track an event execution.
*
* @param event the event to save
*/
public void count(String event) {
logger.debug("Counting new record for event: {}", event);
Instant moment = Instant.now();
service.submit(() -> processEvent(event, moment));

}

/**
*
* @param event the event to save
* @param happenedAt the moment when the event is dispatched
*/
private void processEvent(String event, Instant happenedAt) {
database.write(context -> context.newRecord(MetricEvents.METRIC_EVENTS)
.setEvent(event)
.setHappenedAt(happenedAt)
.insert());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Analytics system for collecting and persisting bot activity metrics.
* <p>
* This package provides services and components that record events for later analysis and reporting
* across multiple feature areas.
*/
@MethodsReturnNonnullByDefault
@ParametersAreNonnullByDefault
package org.togetherjava.tjbot.features.analytics;

import org.togetherjava.tjbot.annotations.MethodsReturnNonnullByDefault;

import javax.annotation.ParametersAreNonnullByDefault;
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback;
import net.dv8tion.jda.api.interactions.components.ComponentInteraction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import org.slf4j.Logger;
Expand All @@ -42,6 +41,7 @@
import org.togetherjava.tjbot.features.UserInteractionType;
import org.togetherjava.tjbot.features.UserInteractor;
import org.togetherjava.tjbot.features.VoiceReceiver;
import org.togetherjava.tjbot.features.analytics.Metrics;
import org.togetherjava.tjbot.features.componentids.ComponentId;
import org.togetherjava.tjbot.features.componentids.ComponentIdParser;
import org.togetherjava.tjbot.features.componentids.ComponentIdStore;
Expand Down Expand Up @@ -79,13 +79,13 @@ public final class BotCore extends ListenerAdapter implements CommandProvider {
private static final ExecutorService COMMAND_SERVICE = Executors.newCachedThreadPool();
private static final ScheduledExecutorService ROUTINE_SERVICE =
Executors.newScheduledThreadPool(5);
private final Config config;
private final Map<String, UserInteractor> prefixedNameToInteractor;
private final List<Routine> routines;
private final ComponentIdParser componentIdParser;
private final ComponentIdStore componentIdStore;
private final Map<Pattern, MessageReceiver> channelNameToMessageReceiver = new HashMap<>();
private final Map<Pattern, VoiceReceiver> channelNameToVoiceReceiver = new HashMap<>();
private final Metrics metrics;

/**
* Creates a new command system which uses the given database to allow commands to persist data.
Expand All @@ -95,10 +95,11 @@ public final class BotCore extends ListenerAdapter implements CommandProvider {
* @param jda the JDA instance that this command system will be used with
* @param database the database that commands may use to persist data
* @param config the configuration to use for this system
* @param metrics the metrics service for tracking analytics
*/
public BotCore(JDA jda, Database database, Config config) {
this.config = config;
Collection<Feature> features = Features.createFeatures(jda, database, config);
public BotCore(JDA jda, Database database, Config config, Metrics metrics) {
this.metrics = metrics;
Collection<Feature> features = Features.createFeatures(jda, database, config, metrics);

// Message receivers
features.stream()
Expand Down Expand Up @@ -300,14 +301,14 @@ private Optional<Channel> selectPreferredAudioChannel(@Nullable AudioChannelUnio
}

@Override
public void onGuildVoiceUpdate(@NotNull GuildVoiceUpdateEvent event) {
public void onGuildVoiceUpdate(GuildVoiceUpdateEvent event) {
selectPreferredAudioChannel(event.getChannelJoined(), event.getChannelLeft())
.ifPresent(channel -> getVoiceReceiversSubscribedTo(channel)
.forEach(voiceReceiver -> voiceReceiver.onVoiceUpdate(event)));
}

@Override
public void onGuildVoiceVideo(@NotNull GuildVoiceVideoEvent event) {
public void onGuildVoiceVideo(GuildVoiceVideoEvent event) {
AudioChannelUnion channel = event.getVoiceState().getChannel();

if (channel == null) {
Expand All @@ -319,7 +320,7 @@ public void onGuildVoiceVideo(@NotNull GuildVoiceVideoEvent event) {
}

@Override
public void onGuildVoiceStream(@NotNull GuildVoiceStreamEvent event) {
public void onGuildVoiceStream(GuildVoiceStreamEvent event) {
AudioChannelUnion channel = event.getVoiceState().getChannel();

if (channel == null) {
Expand All @@ -331,7 +332,7 @@ public void onGuildVoiceStream(@NotNull GuildVoiceStreamEvent event) {
}

@Override
public void onGuildVoiceMute(@NotNull GuildVoiceMuteEvent event) {
public void onGuildVoiceMute(GuildVoiceMuteEvent event) {
AudioChannelUnion channel = event.getVoiceState().getChannel();

if (channel == null) {
Expand All @@ -343,7 +344,7 @@ public void onGuildVoiceMute(@NotNull GuildVoiceMuteEvent event) {
}

@Override
public void onGuildVoiceDeafen(@NotNull GuildVoiceDeafenEvent event) {
public void onGuildVoiceDeafen(GuildVoiceDeafenEvent event) {
AudioChannelUnion channel = event.getVoiceState().getChannel();

if (channel == null) {
Expand Down Expand Up @@ -380,10 +381,16 @@ public void onSlashCommandInteraction(SlashCommandInteractionEvent event) {

logger.debug("Received slash command '{}' (#{}) on guild '{}'", name, event.getId(),
event.getGuild());
COMMAND_SERVICE.execute(
() -> requireUserInteractor(UserInteractionType.SLASH_COMMAND.getPrefixedName(name),
SlashCommand.class)
.onSlashCommand(event));
COMMAND_SERVICE.execute(() -> {

SlashCommand interactor = requireUserInteractor(
UserInteractionType.SLASH_COMMAND.getPrefixedName(name), SlashCommand.class);

metrics.count("slash-" + name);

interactor.onSlashCommand(event);

});
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE metric_events
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
event TEXT NOT NULL,
happened_at TIMESTAMP NOT NULL
);
Loading