From 932481d270b31df488b8018b247343b664865a68 Mon Sep 17 00:00:00 2001 From: yuu1111 Date: Fri, 8 May 2026 05:41:34 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=E8=AA=AD=E3=81=BF=E4=B8=8A=E3=81=92?= =?UTF-8?q?=E6=96=87=E5=AD=97=E6=95=B0=E3=81=AE=E9=9B=86=E8=A8=88=E3=81=A8?= =?UTF-8?q?Prometheus=E3=83=A1=E3=83=88=E3=83=AA=E3=82=AF=E3=82=B9?= =?UTF-8?q?=E5=85=AC=E9=96=8B=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bot/サーバー単位 + 日次で読み上げ文字数とメッセージ数をDBに集計記録する。 SQLite/MySQL双方でアトミックUPSERTにより同時書き込みでもロストしない。 - 新規テーブル tts_count_data (bot_id, server_id, target_date 単位) - TTSCountRecorder で読み上げ確定時に非同期記録 - Micrometer Prometheus レジストリで /metrics エンドポイント公開 (デフォルト 127.0.0.1:9095, 設定で無効化可) - /stat コマンド (today/week/all/server) で Bot 所有者向けに集計表示 - 起動時に DB 累計値を Counter に注入し再起動後も整合 --- core/build.gradle.kts | 3 + .../dev/felnull/itts/core/ITTSRuntime.java | 92 +++++++ .../itts/core/audio/VoiceAudioScheduler.java | 12 +- .../dev/felnull/itts/core/config/Config.java | 9 + .../itts/core/config/MetricsConfig.java | 66 +++++ .../dev/felnull/itts/core/discord/Bot.java | 1 + .../core/discord/command/StatCommand.java | 152 ++++++++++++ .../itts/core/metrics/MetricsRegistry.java | 103 ++++++++ .../core/metrics/PrometheusHttpExposer.java | 67 +++++ .../itts/core/metrics/package-info.java | 4 + .../felnull/itts/core/savedata/dao/DAO.java | 90 +++++++ .../core/savedata/dao/TTSCountRecord.java | 22 ++ .../itts/core/savedata/dao/impl/MySQLDAO.java | 231 ++++++++++++++++++ .../core/savedata/dao/impl/SQLiteDAO.java | 231 ++++++++++++++++++ .../savedata/repository/DataRepository.java | 77 ++++++ .../savedata/repository/TTSCountData.java | 29 +++ .../repository/impl/DataRepositoryImpl.java | 99 ++++++++ .../repository/impl/TTSCountDataImpl.java | 66 +++++ .../itts/core/tts/TTSCountRecorder.java | 75 ++++++ .../savedata/repository/TTSCountDataTest.java | 123 ++++++++++ .../dev/felnull/itts/config/ConfigImpl.java | 69 +++++- 21 files changed, 1616 insertions(+), 5 deletions(-) create mode 100644 core/src/main/java/dev/felnull/itts/core/config/MetricsConfig.java create mode 100644 core/src/main/java/dev/felnull/itts/core/discord/command/StatCommand.java create mode 100644 core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java create mode 100644 core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java create mode 100644 core/src/main/java/dev/felnull/itts/core/metrics/package-info.java create mode 100644 core/src/main/java/dev/felnull/itts/core/savedata/dao/TTSCountRecord.java create mode 100644 core/src/main/java/dev/felnull/itts/core/savedata/repository/TTSCountData.java create mode 100644 core/src/main/java/dev/felnull/itts/core/savedata/repository/impl/TTSCountDataImpl.java create mode 100644 core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java create mode 100644 core/src/test/java/dev/felnull/itts/core/savedata/repository/TTSCountDataTest.java diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 32f6aa4..d31de45 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -37,6 +37,9 @@ dependencies { api("org.xerial:sqlite-jdbc:3.51.1.0") api("it.unimi.dsi:fastutil:8.5.18") + api("io.micrometer:micrometer-core:1.13.6") + api("io.micrometer:micrometer-registry-prometheus:1.13.6") + api("org.jetbrains:annotations:26.0.2-1") // api("org.apache.logging.log4j:log4j-core:3.0.0-beta3") diff --git a/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java b/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java index 16259b0..2e709fa 100644 --- a/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java +++ b/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java @@ -4,9 +4,14 @@ import dev.felnull.itts.core.audio.VoiceAudioManager; import dev.felnull.itts.core.cache.CacheManager; import dev.felnull.itts.core.config.ConfigManager; +import dev.felnull.itts.core.config.MetricsConfig; import dev.felnull.itts.core.dict.DictionaryManager; import dev.felnull.itts.core.discord.Bot; +import dev.felnull.itts.core.metrics.MetricsRegistry; +import dev.felnull.itts.core.metrics.PrometheusHttpExposer; import dev.felnull.itts.core.savedata.SaveDataManager; +import dev.felnull.itts.core.savedata.repository.DataRepository; +import dev.felnull.itts.core.tts.TTSCountRecorder; import dev.felnull.itts.core.tts.TTSManager; import dev.felnull.itts.core.voice.VoiceManager; import org.apache.commons.lang3.concurrent.BasicThreadFactory; @@ -129,6 +134,21 @@ public class ITTSRuntime { */ private long startupTime; + /** + * Prometheusメトリクスのレジストリ + */ + private MetricsRegistry metricsRegistry; + + /** + * Prometheusメトリクス公開HTTPサーバー + */ + private PrometheusHttpExposer prometheusHttpExposer; + + /** + * 読み上げ文字数のレコーダー + */ + private TTSCountRecorder ttsCountRecorder; + private ITTSRuntime(ITTSRuntimeContext runtimeContext) { if (instance != null) { throw new IllegalStateException("ITTSRuntime must be a singleton instance"); @@ -202,9 +222,63 @@ public void execute() { logger.info("Setup complete"); + initMetrics(); + bot.start(); } + private void initMetrics() { + MetricsConfig metricsConfig = configManager.getConfig().getMetricsConfig(); + this.metricsRegistry = metricsConfig.isEnabled() ? new MetricsRegistry() : null; + this.ttsCountRecorder = new TTSCountRecorder(metricsRegistry); + + if (metricsRegistry == null) { + logger.info("Prometheus metrics is disabled"); + return; + } + + try { + this.prometheusHttpExposer = new PrometheusHttpExposer(metricsRegistry); + this.prometheusHttpExposer.start(metricsConfig.getBindAddress(), metricsConfig.getPort()); + logger.info("Prometheus metrics endpoint started on {}:{}/metrics", metricsConfig.getBindAddress(), metricsConfig.getPort()); + warmupMetricsCounters(); + } catch (Exception e) { + logger.warn("Failed to start Prometheus HTTP exposer", e); + this.prometheusHttpExposer = null; + } + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + if (this.prometheusHttpExposer != null) { + this.prometheusHttpExposer.stop(); + } + }, "prometheus-exposer-shutdown")); + } + + private void warmupMetricsCounters() { + if (metricsRegistry == null) { + return; + } + + DataRepository repo = SaveDataManager.getInstance().getRepository(); + if (repo == null) { + return; + } + + try { + long botId = bot.getBotId(); + long charTotal = repo.sumGlobalAllCharCount(botId); + long messageTotal = repo.sumGlobalAllMessageCount(botId); + if (charTotal > 0) { + metricsRegistry.getOrCreateCharCounter(botId, null).increment(charTotal); + } + if (messageTotal > 0) { + metricsRegistry.getOrCreateMessageCounter(botId, null).increment(messageTotal); + } + } catch (Throwable t) { + logger.warn("Failed to warmup metrics counters", t); + } + } + public long getStartupTime() { return startupTime; } @@ -273,4 +347,22 @@ public ITTSNetworkManager getNetworkManager() { public Bot getBot() { return bot; } + + /** + * 読み上げ文字数のレコーダーを取得 + * + * @return レコーダー + */ + public TTSCountRecorder getTTSCountRecorder() { + return ttsCountRecorder; + } + + /** + * Prometheusメトリクスのレジストリを取得 + * + * @return レジストリ nullの場合はメトリクス無効 + */ + public MetricsRegistry getMetricsRegistry() { + return metricsRegistry; + } } diff --git a/core/src/main/java/dev/felnull/itts/core/audio/VoiceAudioScheduler.java b/core/src/main/java/dev/felnull/itts/core/audio/VoiceAudioScheduler.java index b47b31b..a7512a7 100644 --- a/core/src/main/java/dev/felnull/itts/core/audio/VoiceAudioScheduler.java +++ b/core/src/main/java/dev/felnull/itts/core/audio/VoiceAudioScheduler.java @@ -4,8 +4,10 @@ import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason; +import dev.felnull.itts.core.ITTSRuntime; import dev.felnull.itts.core.ITTSRuntimeUse; import dev.felnull.itts.core.audio.loader.VoiceTrackLoader; +import dev.felnull.itts.core.tts.TTSCountRecorder; import dev.felnull.itts.core.tts.saidtext.SaidText; import dev.felnull.itts.core.util.TTSUtils; import dev.felnull.itts.core.voice.Voice; @@ -94,7 +96,15 @@ public CompletableFuture load(SaidText saidText) { Objects.requireNonNull(voice, "Voice is null"); - return Pair.of(TTSUtils.roundText(voice, guildId, sayText, false), voice); + String finalText = TTSUtils.roundText(voice, guildId, sayText, false); + + TTSCountRecorder recorder = ITTSRuntime.getInstance().getTTSCountRecorder(); + if (recorder != null && finalText != null) { + long botId = audioManager.getGuild().getJDA().getSelfUser().getIdLong(); + recorder.record(botId, guildId, finalText.length()); + } + + return Pair.of(finalText, voice); }, getAsyncExecutor()) .thenComposeAsync((sayTextVoice) -> { VoiceTrackLoader vtl = sayTextVoice.getRight().createVoiceTrackLoader(sayTextVoice.getLeft()); diff --git a/core/src/main/java/dev/felnull/itts/core/config/Config.java b/core/src/main/java/dev/felnull/itts/core/config/Config.java index 5d68ede..6a733d0 100644 --- a/core/src/main/java/dev/felnull/itts/core/config/Config.java +++ b/core/src/main/java/dev/felnull/itts/core/config/Config.java @@ -82,4 +82,13 @@ public interface Config { * @return DB関係のコンフィグ */ DataBaseConfig getDataBaseConfig(); + + /** + * Prometheusメトリクスのコンフィグを取得 + * + * @return メトリクスコンフィグ + */ + default MetricsConfig getMetricsConfig() { + return MetricsConfig.DEFAULT; + } } diff --git a/core/src/main/java/dev/felnull/itts/core/config/MetricsConfig.java b/core/src/main/java/dev/felnull/itts/core/config/MetricsConfig.java new file mode 100644 index 0000000..7516723 --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/config/MetricsConfig.java @@ -0,0 +1,66 @@ +package dev.felnull.itts.core.config; + +import org.jetbrains.annotations.NotNull; + +/** + * Prometheusメトリクス公開のコンフィグ + */ +public interface MetricsConfig { + + /** + * デフォルトの有効状態 + */ + boolean DEFAULT_ENABLED = false; + + /** + * デフォルトのバインドアドレス + */ + String DEFAULT_BIND_ADDRESS = "127.0.0.1"; + + /** + * デフォルトのポート番号 + */ + int DEFAULT_PORT = 9095; + + /** + * デフォルトのコンフィグインスタンス + */ + MetricsConfig DEFAULT = new MetricsConfig() { + @Override + public boolean isEnabled() { + return DEFAULT_ENABLED; + } + + @Override + public @NotNull String getBindAddress() { + return DEFAULT_BIND_ADDRESS; + } + + @Override + public int getPort() { + return DEFAULT_PORT; + } + }; + + /** + * 有効かどうかを取得 + * + * @return 有効かどうか + */ + boolean isEnabled(); + + /** + * バインドアドレスを取得 + * + * @return バインドアドレス + */ + @NotNull + String getBindAddress(); + + /** + * ポート番号を取得 + * + * @return ポート番号 + */ + int getPort(); +} diff --git a/core/src/main/java/dev/felnull/itts/core/discord/Bot.java b/core/src/main/java/dev/felnull/itts/core/discord/Bot.java index 7f38fc9..ac1dab7 100644 --- a/core/src/main/java/dev/felnull/itts/core/discord/Bot.java +++ b/core/src/main/java/dev/felnull/itts/core/discord/Bot.java @@ -74,6 +74,7 @@ private void registeringCommands() { registerCommand(new AdminCommand()); registerCommand(new DictCommand()); registerCommand(new SkipCommand()); + registerCommand(new StatCommand()); } private void registerCommand(BaseCommand command) { diff --git a/core/src/main/java/dev/felnull/itts/core/discord/command/StatCommand.java b/core/src/main/java/dev/felnull/itts/core/discord/command/StatCommand.java new file mode 100644 index 0000000..7e8b114 --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/discord/command/StatCommand.java @@ -0,0 +1,152 @@ +package dev.felnull.itts.core.discord.command; + +import dev.felnull.itts.core.savedata.SaveDataManager; +import dev.felnull.itts.core.savedata.repository.DataRepository; +import dev.felnull.itts.core.savedata.repository.TTSCountData; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.InteractionContextType; +import net.dv8tion.jda.api.interactions.commands.build.Commands; +import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.Objects; + +/** + * 読み上げ統計表示コマンド + */ +public class StatCommand extends BaseCommand { + + /** + * 時間を相対的に表示するフォーマット + */ + private static final String RELATIVE_TIME_FORMAT = ""; + + /** + * コンストラクタ + */ + public StatCommand() { + super("stat"); + } + + @NotNull + @Override + public SlashCommandData createSlashCommand() { + return Commands.slash("stat", "読み上げ統計を表示") + .setContexts(InteractionContextType.GUILD) + .setDefaultPermissions(OWNERS_PERMISSIONS) + .addSubcommands(new SubcommandData("today", "本日のBOT全体の読み上げ統計")) + .addSubcommands(new SubcommandData("week", "過去7日のBOT全体の読み上げ統計")) + .addSubcommands(new SubcommandData("all", "BOT全体の累計統計")) + .addSubcommands(new SubcommandData("server", "現在のサーバーの累計統計")); + } + + @Override + public void commandInteraction(SlashCommandInteractionEvent event) { + switch (Objects.requireNonNull(event.getSubcommandName())) { + case "today" -> today(event); + case "week" -> week(event); + case "all" -> all(event); + case "server" -> server(event); + default -> { + } + } + } + + private void today(SlashCommandInteractionEvent event) { + DataRepository repo = SaveDataManager.getInstance().getRepository(); + long botId = event.getJDA().getSelfUser().getIdLong(); + LocalDate today = LocalDate.now(ZoneOffset.UTC); + TTSCountData data = repo.getGlobalTTSCount(botId, today); + + EmbedBuilder builder = baseEmbed("本日の読み上げ統計 (UTC)"); + builder.addField("文字数", data.getCharCount() + "文字", true); + builder.addField("メッセージ数", data.getMessageCount() + "件", true); + addUptimeFields(builder); + + event.replyEmbeds(builder.build()).setEphemeral(true).queue(); + } + + private void week(SlashCommandInteractionEvent event) { + DataRepository repo = SaveDataManager.getInstance().getRepository(); + long botId = event.getJDA().getSelfUser().getIdLong(); + LocalDate today = LocalDate.now(ZoneOffset.UTC); + LocalDate from = today.minusDays(6); + + long charSum = repo.sumGlobalCharCount(botId, from, today); + + EmbedBuilder builder = baseEmbed("過去7日の読み上げ統計 (UTC)"); + builder.addField("期間", from + " - " + today, false); + builder.addField("文字数", charSum + "文字", true); + addUptimeFields(builder); + + event.replyEmbeds(builder.build()).setEphemeral(true).queue(); + } + + private void all(SlashCommandInteractionEvent event) { + DataRepository repo = SaveDataManager.getInstance().getRepository(); + long botId = event.getJDA().getSelfUser().getIdLong(); + + long charSum = repo.sumGlobalAllCharCount(botId); + long messageSum = repo.sumGlobalAllMessageCount(botId); + + EmbedBuilder builder = baseEmbed("BOT全体の累計読み上げ統計"); + builder.addField("累計文字数", charSum + "文字", true); + builder.addField("累計メッセージ数", messageSum + "件", true); + addUptimeFields(builder); + + event.replyEmbeds(builder.build()).setEphemeral(true).queue(); + } + + private void server(SlashCommandInteractionEvent event) { + Guild guild = Objects.requireNonNull(event.getGuild()); + DataRepository repo = SaveDataManager.getInstance().getRepository(); + long botId = event.getJDA().getSelfUser().getIdLong(); + long serverId = guild.getIdLong(); + + long charSum = repo.sumServerAllCharCount(botId, serverId); + long messageSum = repo.sumServerAllMessageCount(botId, serverId); + + EmbedBuilder builder = baseEmbed("このサーバーの累計読み上げ統計"); + builder.addField("サーバー名", guild.getName(), false); + builder.addField("累計文字数", charSum + "文字", true); + builder.addField("累計メッセージ数", messageSum + "件", true); + addUptimeFields(builder); + + event.replyEmbeds(builder.build()).setEphemeral(true).queue(); + } + + private EmbedBuilder baseEmbed(String title) { + EmbedBuilder builder = new EmbedBuilder(); + builder.setColor(getConfigManager().getConfig().getThemeColor()); + builder.setTitle(title); + return builder; + } + + private void addUptimeFields(EmbedBuilder builder) { + long startup = getITTSRuntime().getStartupTime(); + long now = System.currentTimeMillis(); + Duration uptime = Duration.ofMillis(now - startup); + builder.addField("稼働開始", String.format(RELATIVE_TIME_FORMAT, startup / 1000), true); + builder.addField("稼働時間", formatDuration(uptime), true); + } + + private String formatDuration(Duration duration) { + long days = duration.toDays(); + long hours = duration.toHoursPart(); + long minutes = duration.toMinutesPart(); + long seconds = duration.toSecondsPart(); + if (days > 0) { + return days + "日" + hours + "時間" + minutes + "分"; + } + if (hours > 0) { + return hours + "時間" + minutes + "分" + seconds + "秒"; + } + return minutes + "分" + seconds + "秒"; + } +} diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java b/core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java new file mode 100644 index 0000000..011a58b --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java @@ -0,0 +1,103 @@ +package dev.felnull.itts.core.metrics; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; +import io.micrometer.core.instrument.binder.system.ProcessorMetrics; +import io.micrometer.prometheusmetrics.PrometheusConfig; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Prometheusメトリクスのレジストリ管理 + */ +public final class MetricsRegistry { + + /** + * Prometheus形式のレジストリ + */ + private final PrometheusMeterRegistry registry; + + /** + * 起動時刻のミリ秒 + */ + private final long bootAt; + + /** + * 文字数Counterのキャッシュ + */ + private final ConcurrentMap charCounters = new ConcurrentHashMap<>(); + + /** + * メッセージ数Counterのキャッシュ + */ + private final ConcurrentMap messageCounters = new ConcurrentHashMap<>(); + + /** + * コンストラクタ + */ + public MetricsRegistry() { + this.registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); + this.bootAt = System.currentTimeMillis(); + + Gauge.builder("itts_up", () -> 1.0d) + .description("Bot liveness indicator") + .register(registry); + + Gauge.builder("itts_uptime_seconds", () -> (System.currentTimeMillis() - bootAt) / 1000.0d) + .description("Bot uptime in seconds") + .register(registry); + + new JvmMemoryMetrics().bindTo(registry); + new JvmGcMetrics().bindTo(registry); + new ProcessorMetrics().bindTo(registry); + } + + /** + * Prometheusレジストリを取得 + * + * @return レジストリ + */ + @NotNull + public PrometheusMeterRegistry getRegistry() { + return registry; + } + + /** + * 文字数Counterをラベル付きで取得 + * + * @param botId BOTのID + * @param serverId サーバーID nullの場合はBOT全体 + * @return Counter + */ + @NotNull + public Counter getOrCreateCharCounter(long botId, Long serverId) { + String key = botId + "|" + (serverId == null ? "global" : serverId.toString()); + return charCounters.computeIfAbsent(key, k -> Counter.builder("itts_spoken_chars_total") + .description("Total spoken characters delivered to TTS API") + .tag("bot_id", String.valueOf(botId)) + .tag("server_id", serverId == null ? "global" : String.valueOf(serverId)) + .register(registry)); + } + + /** + * メッセージ数Counterをラベル付きで取得 + * + * @param botId BOTのID + * @param serverId サーバーID nullの場合はBOT全体 + * @return Counter + */ + @NotNull + public Counter getOrCreateMessageCounter(long botId, Long serverId) { + String key = botId + "|" + (serverId == null ? "global" : serverId.toString()); + return messageCounters.computeIfAbsent(key, k -> Counter.builder("itts_spoken_messages_total") + .description("Total spoken messages delivered to TTS API") + .tag("bot_id", String.valueOf(botId)) + .tag("server_id", serverId == null ? "global" : String.valueOf(serverId)) + .register(registry)); + } +} diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java b/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java new file mode 100644 index 0000000..71590dc --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java @@ -0,0 +1,67 @@ +package dev.felnull.itts.core.metrics; + +import com.sun.net.httpserver.HttpServer; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +/** + * PrometheusメトリクスをHTTPで公開するエクスポーザ + */ +public final class PrometheusHttpExposer { + + /** + * メトリクスレジストリ + */ + private final MetricsRegistry metricsRegistry; + + /** + * 内部HTTPサーバー + */ + private HttpServer httpServer; + + /** + * コンストラクタ + * + * @param metricsRegistry メトリクスレジストリ + */ + public PrometheusHttpExposer(@NotNull MetricsRegistry metricsRegistry) { + this.metricsRegistry = metricsRegistry; + } + + /** + * 起動する + * + * @param host バインドアドレス + * @param port ポート番号 + * @throws IOException 起動失敗時 + */ + public void start(@NotNull String host, int port) throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(host, port), 0); + server.createContext("/metrics", exchange -> { + String body = metricsRegistry.getRegistry().scrape(); + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", "text/plain; version=0.0.4; charset=utf-8"); + exchange.sendResponseHeaders(200, bytes.length); + try (OutputStream out = exchange.getResponseBody()) { + out.write(bytes); + } + }); + server.setExecutor(null); + server.start(); + this.httpServer = server; + } + + /** + * 停止する + */ + public void stop() { + if (httpServer != null) { + httpServer.stop(0); + httpServer = null; + } + } +} diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/package-info.java b/core/src/main/java/dev/felnull/itts/core/metrics/package-info.java new file mode 100644 index 0000000..8ab66c6 --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/metrics/package-info.java @@ -0,0 +1,4 @@ +/** + * Prometheusメトリクスの収集と公開 + */ +package dev.felnull.itts.core.metrics; diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/dao/DAO.java b/core/src/main/java/dev/felnull/itts/core/savedata/dao/DAO.java index 71f8da9..c60c610 100644 --- a/core/src/main/java/dev/felnull/itts/core/savedata/dao/DAO.java +++ b/core/src/main/java/dev/felnull/itts/core/savedata/dao/DAO.java @@ -8,6 +8,7 @@ import java.sql.Connection; import java.sql.SQLException; +import java.time.LocalDate; import java.util.List; import java.util.Map; import java.util.Optional; @@ -135,6 +136,13 @@ public interface DAO { */ GlobalCustomDictionaryTable globalCustomDictionaryTable(); + /** + * 読み上げ文字数集計テーブル + * + * @return テーブルインスタンス + */ + TTSCountTable ttsCountTable(); + /** * 絵文字をサポートしているか確認 * @@ -767,4 +775,86 @@ interface GlobalCustomDictionaryTable extends Table { Map selectRecordByTarget(Connection connection, @NotNull String targetWord) throws SQLException; } + /** + * 読み上げ文字数集計テーブル + */ + interface TTSCountTable extends Table { + + /** + * 指定日のカウントを増分する + * + * @param connection コネクション + * @param botKeyId BOTキーID + * @param serverKeyId サーバーキーID nullの場合はBOT全体合計 + * @param date 集計日 + * @param charDelta 文字数の増分 + * @param messageDelta メッセージ数の増分 + * @throws SQLException エラー + */ + void incrementCount(@NotNull Connection connection, + int botKeyId, + @Nullable Integer serverKeyId, + @NotNull LocalDate date, + long charDelta, + long messageDelta) throws SQLException; + + /** + * 指定日のカウントを取得する + * + * @param connection コネクション + * @param botKeyId BOTキーID + * @param serverKeyId サーバーキーID nullの場合はBOT全体合計 + * @param date 集計日 + * @return カウントレコード + * @throws SQLException エラー + */ + Optional getCount(@NotNull Connection connection, + int botKeyId, + @Nullable Integer serverKeyId, + @NotNull LocalDate date) throws SQLException; + + /** + * 期間内の文字数合計を取得する + * + * @param connection コネクション + * @param botKeyId BOTキーID + * @param serverKeyId サーバーキーID nullの場合はBOT全体合計 + * @param from 開始日 (含む) + * @param to 終了日 (含む) + * @return 文字数とメッセージ数の合計 + * @throws SQLException エラー + */ + long sumCharCountRange(@NotNull Connection connection, + int botKeyId, + @Nullable Integer serverKeyId, + @NotNull LocalDate from, + @NotNull LocalDate to) throws SQLException; + + /** + * 全期間の文字数合計を取得する + * + * @param connection コネクション + * @param botKeyId BOTキーID + * @param serverKeyId サーバーキーID nullの場合はBOT全体合計 + * @return 文字数の合計 + * @throws SQLException エラー + */ + long sumAllCharCount(@NotNull Connection connection, + int botKeyId, + @Nullable Integer serverKeyId) throws SQLException; + + /** + * 全期間のメッセージ数合計を取得する + * + * @param connection コネクション + * @param botKeyId BOTキーID + * @param serverKeyId サーバーキーID nullの場合はBOT全体合計 + * @return メッセージ数の合計 + * @throws SQLException エラー + */ + long sumAllMessageCount(@NotNull Connection connection, + int botKeyId, + @Nullable Integer serverKeyId) throws SQLException; + } + } diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/dao/TTSCountRecord.java b/core/src/main/java/dev/felnull/itts/core/savedata/dao/TTSCountRecord.java new file mode 100644 index 0000000..1d6d35d --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/savedata/dao/TTSCountRecord.java @@ -0,0 +1,22 @@ +package dev.felnull.itts.core.savedata.dao; + +import org.jetbrains.annotations.Nullable; + +import java.time.LocalDate; + +/** + * 読み上げ文字数集計のレコード + * + * @param botId BOTのDiscord ID + * @param serverId サーバーのDiscord ID nullの場合はBOT全体合計 + * @param date 集計日 + * @param charCount 読み上げ文字数 + * @param messageCount 読み上げメッセージ数 + */ +public record TTSCountRecord(long botId, + @Nullable Long serverId, + LocalDate date, + long charCount, + long messageCount +) { +} diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/dao/impl/MySQLDAO.java b/core/src/main/java/dev/felnull/itts/core/savedata/dao/impl/MySQLDAO.java index 6194953..01899bd 100644 --- a/core/src/main/java/dev/felnull/itts/core/savedata/dao/impl/MySQLDAO.java +++ b/core/src/main/java/dev/felnull/itts/core/savedata/dao/impl/MySQLDAO.java @@ -13,6 +13,7 @@ import org.jetbrains.annotations.Unmodifiable; import java.sql.*; +import java.time.LocalDate; import java.util.*; /** @@ -90,6 +91,11 @@ class MySQLDAO extends BaseDAO { */ private final GlobalCustomDictionaryTable globalCustomDictionaryTable = new GlobalCustomDictionaryTableImpl(); + /** + * 読み上げ文字数集計テーブルのインスタンス + */ + private final TTSCountTable ttsCountTable = new TTSCountTableImpl(); + /** * ホスト名 */ @@ -222,6 +228,11 @@ public GlobalCustomDictionaryTable globalCustomDictionaryTable() { return globalCustomDictionaryTable; } + @Override + public TTSCountTable ttsCountTable() { + return ttsCountTable; + } + @Override public boolean checkEmojiSupport() { throw new AssertionError("TODO"); @@ -2447,4 +2458,224 @@ public Map selectRecordByTarget(Connection connection return ret.build(); } } + + /** + * 読み上げ文字数集計テーブルの実装 + */ + private final class TTSCountTableImpl implements TTSCountTable { + + @Override + public void createTableIfNotExists(@NotNull Connection connection) throws SQLException { + @Language("MySQL") + String sql = """ + create table if not exists tts_count_data( + id integer not null primary key auto_increment, + bot_id integer not null, + server_id integer, + target_date date not null, + spoken_char_count bigint not null default 0, + spoken_message_count bigint not null default 0, + + unique(bot_id, server_id, target_date), + foreign key (bot_id) references bot_key(id), + foreign key (server_id) references server_key(id) + ); + """; + + execute(connection, sql); + } + + @Override + public void incrementCount(@NotNull Connection connection, + int botKeyId, + @Nullable Integer serverKeyId, + @NotNull LocalDate date, + long charDelta, + long messageDelta) throws SQLException { + if (serverKeyId == null) { + incrementCountServerNull(connection, botKeyId, date, charDelta, messageDelta); + return; + } + + @Language("MySQL") + String sql = """ + insert into tts_count_data(bot_id, server_id, target_date, spoken_char_count, spoken_message_count) + values (?, ?, ?, ?, ?) + on duplicate key update + spoken_char_count = spoken_char_count + values(spoken_char_count), + spoken_message_count = spoken_message_count + values(spoken_message_count) + """; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, botKeyId); + statement.setInt(2, serverKeyId); + statement.setString(3, date.toString()); + statement.setLong(4, charDelta); + statement.setLong(5, messageDelta); + statement.execute(); + } + } + + private void incrementCountServerNull(@NotNull Connection connection, + int botKeyId, + @NotNull LocalDate date, + long charDelta, + long messageDelta) throws SQLException { + @Language("MySQL") + String updateSql = """ + update tts_count_data + set spoken_char_count = spoken_char_count + ?, + spoken_message_count = spoken_message_count + ? + where bot_id = ? and server_id is null and target_date = ? + """; + + try (PreparedStatement statement = connection.prepareStatement(updateSql)) { + statement.setLong(1, charDelta); + statement.setLong(2, messageDelta); + statement.setInt(3, botKeyId); + statement.setString(4, date.toString()); + + if (statement.executeUpdate() > 0) { + return; + } + } + + @Language("MySQL") + String insertSql = """ + insert into tts_count_data(bot_id, server_id, target_date, spoken_char_count, spoken_message_count) + values (?, null, ?, ?, ?) + """; + + try (PreparedStatement statement = connection.prepareStatement(insertSql)) { + statement.setInt(1, botKeyId); + statement.setString(2, date.toString()); + statement.setLong(3, charDelta); + statement.setLong(4, messageDelta); + statement.execute(); + } + } + + @Override + public Optional getCount(@NotNull Connection connection, + int botKeyId, + @Nullable Integer serverKeyId, + @NotNull LocalDate date) throws SQLException { + @Language("MySQL") + String sqlServer = """ + select spoken_char_count, spoken_message_count + from tts_count_data + where bot_id = ? and server_id = ? and target_date = ? + limit 1 + """; + + @Language("MySQL") + String sqlNull = """ + select spoken_char_count, spoken_message_count + from tts_count_data + where bot_id = ? and server_id is null and target_date = ? + limit 1 + """; + + try (PreparedStatement statement = connection.prepareStatement(serverKeyId == null ? sqlNull : sqlServer)) { + statement.setInt(1, botKeyId); + if (serverKeyId == null) { + statement.setString(2, date.toString()); + } else { + statement.setInt(2, serverKeyId); + statement.setString(3, date.toString()); + } + + try (ResultSet rs = statement.executeQuery()) { + if (rs.next()) { + long ch = rs.getLong("spoken_char_count"); + long ms = rs.getLong("spoken_message_count"); + return Optional.of(new TTSCountRecord(0L, null, date, ch, ms)); + } + } + } + + return Optional.empty(); + } + + @Override + public long sumCharCountRange(@NotNull Connection connection, + int botKeyId, + @Nullable Integer serverKeyId, + @NotNull LocalDate from, + @NotNull LocalDate to) throws SQLException { + @Language("MySQL") + String sqlServer = """ + select coalesce(sum(spoken_char_count), 0) as total + from tts_count_data + where bot_id = ? and server_id = ? and target_date between ? and ? + """; + + @Language("MySQL") + String sqlNull = """ + select coalesce(sum(spoken_char_count), 0) as total + from tts_count_data + where bot_id = ? and server_id is null and target_date between ? and ? + """; + + try (PreparedStatement statement = connection.prepareStatement(serverKeyId == null ? sqlNull : sqlServer)) { + statement.setInt(1, botKeyId); + if (serverKeyId == null) { + statement.setString(2, from.toString()); + statement.setString(3, to.toString()); + } else { + statement.setInt(2, serverKeyId); + statement.setString(3, from.toString()); + statement.setString(4, to.toString()); + } + + try (ResultSet rs = statement.executeQuery()) { + if (rs.next()) { + return rs.getLong("total"); + } + } + } + + return 0L; + } + + @Override + public long sumAllCharCount(@NotNull Connection connection, + int botKeyId, + @Nullable Integer serverKeyId) throws SQLException { + return sumAllByColumn(connection, botKeyId, serverKeyId, "spoken_char_count"); + } + + @Override + public long sumAllMessageCount(@NotNull Connection connection, + int botKeyId, + @Nullable Integer serverKeyId) throws SQLException { + return sumAllByColumn(connection, botKeyId, serverKeyId, "spoken_message_count"); + } + + private long sumAllByColumn(@NotNull Connection connection, + int botKeyId, + @Nullable Integer serverKeyId, + @NotNull String column) throws SQLException { + @Language("MySQL") + String sqlServer = "select coalesce(sum(" + column + "), 0) as total from tts_count_data where bot_id = ? and server_id = ?"; + + @Language("MySQL") + String sqlNull = "select coalesce(sum(" + column + "), 0) as total from tts_count_data where bot_id = ? and server_id is null"; + + try (PreparedStatement statement = connection.prepareStatement(serverKeyId == null ? sqlNull : sqlServer)) { + statement.setInt(1, botKeyId); + if (serverKeyId != null) { + statement.setInt(2, serverKeyId); + } + + try (ResultSet rs = statement.executeQuery()) { + if (rs.next()) { + return rs.getLong("total"); + } + } + } + + return 0L; + } + } } diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/dao/impl/SQLiteDAO.java b/core/src/main/java/dev/felnull/itts/core/savedata/dao/impl/SQLiteDAO.java index bf8c564..ebdad8b 100644 --- a/core/src/main/java/dev/felnull/itts/core/savedata/dao/impl/SQLiteDAO.java +++ b/core/src/main/java/dev/felnull/itts/core/savedata/dao/impl/SQLiteDAO.java @@ -15,6 +15,7 @@ import java.io.File; import java.sql.*; +import java.time.LocalDate; import java.util.*; /** @@ -92,6 +93,11 @@ public class SQLiteDAO extends BaseDAO { */ private final GlobalCustomDictionaryTable globalCustomDictionaryTable = new GlobalCustomDictionaryTableImpl(); + /** + * 読み上げ文字数集計テーブルのインスタンス + */ + private final TTSCountTable ttsCountTable = new TTSCountTableImpl(); + /** * DBファイル */ @@ -206,6 +212,11 @@ public GlobalCustomDictionaryTable globalCustomDictionaryTable() { return globalCustomDictionaryTable; } + @Override + public TTSCountTable ttsCountTable() { + return ttsCountTable; + } + @Override public boolean checkEmojiSupport() { return true; @@ -2433,4 +2444,224 @@ public Map selectRecordByTarget(Connection connection return ret.build(); } } + + /** + * 読み上げ文字数集計テーブルの実装 + */ + private final class TTSCountTableImpl implements TTSCountTable { + + @Override + public void createTableIfNotExists(@NotNull Connection connection) throws SQLException { + @Language("SQLite") + String sql = """ + create table if not exists tts_count_data( + id integer not null primary key autoincrement, + bot_id integer not null, + server_id integer, + target_date date not null, + spoken_char_count bigint not null default 0, + spoken_message_count bigint not null default 0, + + unique(bot_id, server_id, target_date), + foreign key (bot_id) references bot_key(id), + foreign key (server_id) references server_key(id) + ); + """; + + execute(connection, sql); + } + + @Override + public void incrementCount(@NotNull Connection connection, + int botKeyId, + @Nullable Integer serverKeyId, + @NotNull LocalDate date, + long charDelta, + long messageDelta) throws SQLException { + if (serverKeyId == null) { + incrementCountServerNull(connection, botKeyId, date, charDelta, messageDelta); + return; + } + + @Language("SQLite") + String sql = """ + insert into tts_count_data(bot_id, server_id, target_date, spoken_char_count, spoken_message_count) + values (?, ?, ?, ?, ?) + on conflict(bot_id, server_id, target_date) do update set + spoken_char_count = spoken_char_count + excluded.spoken_char_count, + spoken_message_count = spoken_message_count + excluded.spoken_message_count + """; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setInt(1, botKeyId); + statement.setInt(2, serverKeyId); + statement.setString(3, date.toString()); + statement.setLong(4, charDelta); + statement.setLong(5, messageDelta); + statement.execute(); + } + } + + private void incrementCountServerNull(@NotNull Connection connection, + int botKeyId, + @NotNull LocalDate date, + long charDelta, + long messageDelta) throws SQLException { + @Language("SQLite") + String updateSql = """ + update tts_count_data + set spoken_char_count = spoken_char_count + ?, + spoken_message_count = spoken_message_count + ? + where bot_id = ? and server_id is null and target_date = ? + """; + + try (PreparedStatement statement = connection.prepareStatement(updateSql)) { + statement.setLong(1, charDelta); + statement.setLong(2, messageDelta); + statement.setInt(3, botKeyId); + statement.setString(4, date.toString()); + + if (statement.executeUpdate() > 0) { + return; + } + } + + @Language("SQLite") + String insertSql = """ + insert into tts_count_data(bot_id, server_id, target_date, spoken_char_count, spoken_message_count) + values (?, null, ?, ?, ?) + """; + + try (PreparedStatement statement = connection.prepareStatement(insertSql)) { + statement.setInt(1, botKeyId); + statement.setString(2, date.toString()); + statement.setLong(3, charDelta); + statement.setLong(4, messageDelta); + statement.execute(); + } + } + + @Override + public Optional getCount(@NotNull Connection connection, + int botKeyId, + @Nullable Integer serverKeyId, + @NotNull LocalDate date) throws SQLException { + @Language("SQLite") + String sqlServer = """ + select spoken_char_count, spoken_message_count + from tts_count_data + where bot_id = ? and server_id = ? and target_date = ? + limit 1 + """; + + @Language("SQLite") + String sqlNull = """ + select spoken_char_count, spoken_message_count + from tts_count_data + where bot_id = ? and server_id is null and target_date = ? + limit 1 + """; + + try (PreparedStatement statement = connection.prepareStatement(serverKeyId == null ? sqlNull : sqlServer)) { + statement.setInt(1, botKeyId); + if (serverKeyId == null) { + statement.setString(2, date.toString()); + } else { + statement.setInt(2, serverKeyId); + statement.setString(3, date.toString()); + } + + try (ResultSet rs = statement.executeQuery()) { + if (rs.next()) { + long ch = rs.getLong("spoken_char_count"); + long ms = rs.getLong("spoken_message_count"); + return Optional.of(new TTSCountRecord(0L, null, date, ch, ms)); + } + } + } + + return Optional.empty(); + } + + @Override + public long sumCharCountRange(@NotNull Connection connection, + int botKeyId, + @Nullable Integer serverKeyId, + @NotNull LocalDate from, + @NotNull LocalDate to) throws SQLException { + @Language("SQLite") + String sqlServer = """ + select coalesce(sum(spoken_char_count), 0) as total + from tts_count_data + where bot_id = ? and server_id = ? and target_date between ? and ? + """; + + @Language("SQLite") + String sqlNull = """ + select coalesce(sum(spoken_char_count), 0) as total + from tts_count_data + where bot_id = ? and server_id is null and target_date between ? and ? + """; + + try (PreparedStatement statement = connection.prepareStatement(serverKeyId == null ? sqlNull : sqlServer)) { + statement.setInt(1, botKeyId); + if (serverKeyId == null) { + statement.setString(2, from.toString()); + statement.setString(3, to.toString()); + } else { + statement.setInt(2, serverKeyId); + statement.setString(3, from.toString()); + statement.setString(4, to.toString()); + } + + try (ResultSet rs = statement.executeQuery()) { + if (rs.next()) { + return rs.getLong("total"); + } + } + } + + return 0L; + } + + @Override + public long sumAllCharCount(@NotNull Connection connection, + int botKeyId, + @Nullable Integer serverKeyId) throws SQLException { + return sumAllByColumn(connection, botKeyId, serverKeyId, "spoken_char_count"); + } + + @Override + public long sumAllMessageCount(@NotNull Connection connection, + int botKeyId, + @Nullable Integer serverKeyId) throws SQLException { + return sumAllByColumn(connection, botKeyId, serverKeyId, "spoken_message_count"); + } + + private long sumAllByColumn(@NotNull Connection connection, + int botKeyId, + @Nullable Integer serverKeyId, + @NotNull String column) throws SQLException { + @Language("SQLite") + String sqlServer = "select coalesce(sum(" + column + "), 0) as total from tts_count_data where bot_id = ? and server_id = ?"; + + @Language("SQLite") + String sqlNull = "select coalesce(sum(" + column + "), 0) as total from tts_count_data where bot_id = ? and server_id is null"; + + try (PreparedStatement statement = connection.prepareStatement(serverKeyId == null ? sqlNull : sqlServer)) { + statement.setInt(1, botKeyId); + if (serverKeyId != null) { + statement.setInt(2, serverKeyId); + } + + try (ResultSet rs = statement.executeQuery()) { + if (rs.next()) { + return rs.getLong("total"); + } + } + } + + return 0L; + } + } } \ No newline at end of file diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/repository/DataRepository.java b/core/src/main/java/dev/felnull/itts/core/savedata/repository/DataRepository.java index d247241..d454d4c 100644 --- a/core/src/main/java/dev/felnull/itts/core/savedata/repository/DataRepository.java +++ b/core/src/main/java/dev/felnull/itts/core/savedata/repository/DataRepository.java @@ -7,6 +7,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; +import java.time.LocalDate; import java.util.List; import java.util.Map; @@ -147,4 +148,80 @@ static DataRepository create(DAO dao) { @NotNull @Unmodifiable Map getAllBotStateData(long botId); + + /** + * サーバー単位の読み上げ文字数集計データを取得 + * + * @param botId BOTのID + * @param serverId サーバーID + * @param date 集計日 + * @return カウントデータ + */ + @NotNull + TTSCountData getServerTTSCount(long botId, long serverId, @NotNull LocalDate date); + + /** + * BOT全体の読み上げ文字数集計データを取得 + * + * @param botId BOTのID + * @param date 集計日 + * @return カウントデータ + */ + @NotNull + TTSCountData getGlobalTTSCount(long botId, @NotNull LocalDate date); + + /** + * BOT全体の期間内文字数合計を取得 + * + * @param botId BOTのID + * @param from 開始日 + * @param to 終了日 + * @return 文字数合計 + */ + long sumGlobalCharCount(long botId, @NotNull LocalDate from, @NotNull LocalDate to); + + /** + * サーバー単位の期間内文字数合計を取得 + * + * @param botId BOTのID + * @param serverId サーバーID + * @param from 開始日 + * @param to 終了日 + * @return 文字数合計 + */ + long sumServerCharCount(long botId, long serverId, @NotNull LocalDate from, @NotNull LocalDate to); + + /** + * BOT全体の累計文字数を取得 + * + * @param botId BOTのID + * @return 累計文字数 + */ + long sumGlobalAllCharCount(long botId); + + /** + * BOT全体の累計メッセージ数を取得 + * + * @param botId BOTのID + * @return 累計メッセージ数 + */ + long sumGlobalAllMessageCount(long botId); + + /** + * サーバー単位の累計文字数を取得 + * + * @param botId BOTのID + * @param serverId サーバーID + * @return 累計文字数 + */ + long sumServerAllCharCount(long botId, long serverId); + + /** + * サーバー単位の累計メッセージ数を取得 + * + * @param botId BOTのID + * @param serverId サーバーID + * @return 累計メッセージ数 + */ + long sumServerAllMessageCount(long botId, long serverId); } diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/repository/TTSCountData.java b/core/src/main/java/dev/felnull/itts/core/savedata/repository/TTSCountData.java new file mode 100644 index 0000000..1e74fad --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/savedata/repository/TTSCountData.java @@ -0,0 +1,29 @@ +package dev.felnull.itts.core.savedata.repository; + +/** + * 読み上げ文字数集計データ + */ +public interface TTSCountData { + + /** + * カウントを増分する + * + * @param charDelta 文字数の増分 + * @param messageDelta メッセージ数の増分 + */ + void addCount(long charDelta, long messageDelta); + + /** + * 文字数を取得 + * + * @return 文字数 + */ + long getCharCount(); + + /** + * メッセージ数を取得 + * + * @return メッセージ数 + */ + long getMessageCount(); +} diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/repository/impl/DataRepositoryImpl.java b/core/src/main/java/dev/felnull/itts/core/savedata/repository/impl/DataRepositoryImpl.java index de52305..9733ea2 100644 --- a/core/src/main/java/dev/felnull/itts/core/savedata/repository/impl/DataRepositoryImpl.java +++ b/core/src/main/java/dev/felnull/itts/core/savedata/repository/impl/DataRepositoryImpl.java @@ -13,6 +13,7 @@ import java.sql.Connection; import java.sql.SQLException; +import java.time.LocalDate; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -200,6 +201,7 @@ private void initDataBase() throws SQLException { dao.botStateDataTable().createTableIfNotExists(con); dao.serverCustomDictionaryTable().createTableIfNotExists(con); dao.globalCustomDictionaryTable().createTableIfNotExists(con); + dao.ttsCountTable().createTableIfNotExists(con); } } @@ -361,6 +363,103 @@ KeyData getVoiceTypeKeyData() { } } + @Override + public @NotNull TTSCountData getServerTTSCount(long botId, long serverId, @NotNull LocalDate date) { + return new TTSCountDataImpl(this, botId, serverId, date); + } + + @Override + public @NotNull TTSCountData getGlobalTTSCount(long botId, @NotNull LocalDate date) { + return new TTSCountDataImpl(this, botId, null, date); + } + + @Override + public long sumGlobalCharCount(long botId, @NotNull LocalDate from, @NotNull LocalDate to) { + try (Connection connection = dao.getConnection()) { + int botKeyId = botKeyData.getId(botId); + return dao.ttsCountTable().sumCharCountRange(connection, botKeyId, null, from, to); + } catch (Exception e) { + fireErrorEvent(e); + throw new RuntimeException(e); + } catch (Throwable throwable) { + fireErrorEvent(throwable); + throw throwable; + } + } + + @Override + public long sumServerCharCount(long botId, long serverId, @NotNull LocalDate from, @NotNull LocalDate to) { + try (Connection connection = dao.getConnection()) { + int botKeyId = botKeyData.getId(botId); + int serverKeyId = serverKeyData.getId(serverId); + return dao.ttsCountTable().sumCharCountRange(connection, botKeyId, serverKeyId, from, to); + } catch (Exception e) { + fireErrorEvent(e); + throw new RuntimeException(e); + } catch (Throwable throwable) { + fireErrorEvent(throwable); + throw throwable; + } + } + + @Override + public long sumGlobalAllCharCount(long botId) { + try (Connection connection = dao.getConnection()) { + int botKeyId = botKeyData.getId(botId); + return dao.ttsCountTable().sumAllCharCount(connection, botKeyId, null); + } catch (Exception e) { + fireErrorEvent(e); + throw new RuntimeException(e); + } catch (Throwable throwable) { + fireErrorEvent(throwable); + throw throwable; + } + } + + @Override + public long sumGlobalAllMessageCount(long botId) { + try (Connection connection = dao.getConnection()) { + int botKeyId = botKeyData.getId(botId); + return dao.ttsCountTable().sumAllMessageCount(connection, botKeyId, null); + } catch (Exception e) { + fireErrorEvent(e); + throw new RuntimeException(e); + } catch (Throwable throwable) { + fireErrorEvent(throwable); + throw throwable; + } + } + + @Override + public long sumServerAllCharCount(long botId, long serverId) { + try (Connection connection = dao.getConnection()) { + int botKeyId = botKeyData.getId(botId); + int serverKeyId = serverKeyData.getId(serverId); + return dao.ttsCountTable().sumAllCharCount(connection, botKeyId, serverKeyId); + } catch (Exception e) { + fireErrorEvent(e); + throw new RuntimeException(e); + } catch (Throwable throwable) { + fireErrorEvent(throwable); + throw throwable; + } + } + + @Override + public long sumServerAllMessageCount(long botId, long serverId) { + try (Connection connection = dao.getConnection()) { + int botKeyId = botKeyData.getId(botId); + int serverKeyId = serverKeyData.getId(serverId); + return dao.ttsCountTable().sumAllMessageCount(connection, botKeyId, serverKeyId); + } catch (Exception e) { + fireErrorEvent(e); + throw new RuntimeException(e); + } catch (Throwable throwable) { + fireErrorEvent(throwable); + throw throwable; + } + } + /** * サーバーIDとユーザーIDで取得するキャッシュのキー * diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/repository/impl/TTSCountDataImpl.java b/core/src/main/java/dev/felnull/itts/core/savedata/repository/impl/TTSCountDataImpl.java new file mode 100644 index 0000000..ece5532 --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/savedata/repository/impl/TTSCountDataImpl.java @@ -0,0 +1,66 @@ +package dev.felnull.itts.core.savedata.repository.impl; + +import dev.felnull.itts.core.savedata.dao.TTSCountRecord; +import dev.felnull.itts.core.savedata.repository.TTSCountData; +import org.jetbrains.annotations.Nullable; + +import java.time.LocalDate; +import java.util.Optional; + +/** + * 読み上げ文字数集計データの実装 + */ +class TTSCountDataImpl extends SaveDataBase implements TTSCountData { + + /** + * BOT ID + */ + private final long botId; + + /** + * サーバーID nullの場合はBOT全体合計 + */ + @Nullable + private final Long serverId; + + /** + * 集計日 + */ + private final LocalDate date; + + TTSCountDataImpl(DataRepositoryImpl repository, long botId, @Nullable Long serverId, LocalDate date) { + super(repository); + this.botId = botId; + this.serverId = serverId; + this.date = date; + } + + @Override + public void addCount(long charDelta, long messageDelta) { + sqlProc(connection -> { + int botKeyId = repository.getBotKeyData().getId(botId); + Integer serverKeyId = serverId == null ? null : repository.getServerKeyData().getId(serverId); + dao().ttsCountTable().incrementCount(connection, botKeyId, serverKeyId, date, charDelta, messageDelta); + }); + } + + @Override + public long getCharCount() { + return sqlProcReturnable(connection -> { + int botKeyId = repository.getBotKeyData().getId(botId); + Integer serverKeyId = serverId == null ? null : repository.getServerKeyData().getId(serverId); + Optional rec = dao().ttsCountTable().getCount(connection, botKeyId, serverKeyId, date); + return rec.map(TTSCountRecord::charCount).orElse(0L); + }); + } + + @Override + public long getMessageCount() { + return sqlProcReturnable(connection -> { + int botKeyId = repository.getBotKeyData().getId(botId); + Integer serverKeyId = serverId == null ? null : repository.getServerKeyData().getId(serverId); + Optional rec = dao().ttsCountTable().getCount(connection, botKeyId, serverKeyId, date); + return rec.map(TTSCountRecord::messageCount).orElse(0L); + }); + } +} diff --git a/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java b/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java new file mode 100644 index 0000000..d629a55 --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java @@ -0,0 +1,75 @@ +package dev.felnull.itts.core.tts; + +import dev.felnull.itts.core.ITTSRuntimeUse; +import dev.felnull.itts.core.metrics.MetricsRegistry; +import dev.felnull.itts.core.savedata.SaveDataManager; +import dev.felnull.itts.core.savedata.repository.DataRepository; +import dev.felnull.itts.core.savedata.repository.TTSCountData; + +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.concurrent.CompletableFuture; + +/** + * 読み上げ文字数の集計を非同期で記録するレコーダー + */ +public final class TTSCountRecorder implements ITTSRuntimeUse { + + /** + * メトリクスレジストリ nullの場合はメトリクス公開無効 + */ + private final MetricsRegistry metricsRegistry; + + /** + * コンストラクタ + * + * @param metricsRegistry メトリクスレジストリ + */ + public TTSCountRecorder(MetricsRegistry metricsRegistry) { + this.metricsRegistry = metricsRegistry; + } + + /** + * 読み上げ文字数を記録する + * + * @param botDiscordId BOTのDiscord ID + * @param guildDiscordId サーバーのDiscord ID + * @param charCount 文字数 + */ + public void record(long botDiscordId, long guildDiscordId, int charCount) { + if (charCount <= 0) { + return; + } + + if (metricsRegistry != null) { + try { + metricsRegistry.getOrCreateCharCounter(botDiscordId, guildDiscordId).increment(charCount); + metricsRegistry.getOrCreateMessageCounter(botDiscordId, guildDiscordId).increment(); + metricsRegistry.getOrCreateCharCounter(botDiscordId, null).increment(charCount); + metricsRegistry.getOrCreateMessageCounter(botDiscordId, null).increment(); + } catch (Throwable t) { + getITTSLogger().warn("Failed to update metrics counter", t); + } + } + + CompletableFuture.runAsync(() -> writeToDatabase(botDiscordId, guildDiscordId, charCount), getAsyncExecutor()) + .exceptionally(throwable -> { + getITTSLogger().warn("Failed to record TTS count", throwable); + return null; + }); + } + + private void writeToDatabase(long botDiscordId, long guildDiscordId, int charCount) { + DataRepository repo = SaveDataManager.getInstance().getRepository(); + if (repo == null) { + return; + } + + LocalDate today = LocalDate.now(ZoneOffset.UTC); + TTSCountData serverData = repo.getServerTTSCount(botDiscordId, guildDiscordId, today); + TTSCountData globalData = repo.getGlobalTTSCount(botDiscordId, today); + + serverData.addCount(charCount, 1L); + globalData.addCount(charCount, 1L); + } +} diff --git a/core/src/test/java/dev/felnull/itts/core/savedata/repository/TTSCountDataTest.java b/core/src/test/java/dev/felnull/itts/core/savedata/repository/TTSCountDataTest.java new file mode 100644 index 0000000..2ddf499 --- /dev/null +++ b/core/src/test/java/dev/felnull/itts/core/savedata/repository/TTSCountDataTest.java @@ -0,0 +1,123 @@ +package dev.felnull.itts.core.savedata.repository; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TTSCountDataTest extends RepoBaseTest { + + @Test + void testServerAndGlobalAccumulation() { + DataRepository repo = createRepository(); + long botId = 1234567890123L; + long serverId = 9876543210987L; + LocalDate date = LocalDate.now(ZoneOffset.UTC); + + TTSCountData server = repo.getServerTTSCount(botId, serverId, date); + TTSCountData global = repo.getGlobalTTSCount(botId, date); + + assertEquals(0L, server.getCharCount()); + assertEquals(0L, server.getMessageCount()); + assertEquals(0L, global.getCharCount()); + assertEquals(0L, global.getMessageCount()); + + server.addCount(10L, 1L); + server.addCount(5L, 1L); + global.addCount(15L, 2L); + + assertEquals(15L, server.getCharCount()); + assertEquals(2L, server.getMessageCount()); + assertEquals(15L, global.getCharCount()); + assertEquals(2L, global.getMessageCount()); + + repo.dispose(); + } + + @Test + void testRangeAndAllSums() { + DataRepository repo = createRepository(); + long botId = 11111L; + long serverId = 22222L; + LocalDate today = LocalDate.now(ZoneOffset.UTC); + LocalDate yesterday = today.minusDays(1); + + repo.getServerTTSCount(botId, serverId, today).addCount(20L, 2L); + repo.getServerTTSCount(botId, serverId, yesterday).addCount(7L, 1L); + repo.getGlobalTTSCount(botId, today).addCount(20L, 2L); + repo.getGlobalTTSCount(botId, yesterday).addCount(7L, 1L); + + assertEquals(27L, repo.sumServerCharCount(botId, serverId, yesterday, today)); + assertEquals(20L, repo.sumServerCharCount(botId, serverId, today, today)); + assertEquals(27L, repo.sumGlobalCharCount(botId, yesterday, today)); + assertEquals(27L, repo.sumServerAllCharCount(botId, serverId)); + assertEquals(3L, repo.sumServerAllMessageCount(botId, serverId)); + assertEquals(27L, repo.sumGlobalAllCharCount(botId)); + assertEquals(3L, repo.sumGlobalAllMessageCount(botId)); + + repo.dispose(); + } + + @Test + void testConcurrentIncrementNoLostWrites() throws InterruptedException { + DataRepository repo = createRepository(); + long botId = 555L; + long serverId = 666L; + LocalDate date = LocalDate.now(ZoneOffset.UTC); + + int threadCount = 8; + int incrementsPerThread = 50; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch start = new CountDownLatch(1); + CountDownLatch done = new CountDownLatch(threadCount); + AtomicInteger errors = new AtomicInteger(); + List errorList = new ArrayList<>(); + + for (int i = 0; i < threadCount; i++) { + executor.submit(() -> { + try { + start.await(); + TTSCountData server = repo.getServerTTSCount(botId, serverId, date); + TTSCountData global = repo.getGlobalTTSCount(botId, date); + for (int j = 0; j < incrementsPerThread; j++) { + server.addCount(1L, 1L); + global.addCount(1L, 1L); + } + } catch (Throwable t) { + errors.incrementAndGet(); + synchronized (errorList) { + errorList.add(t); + } + } finally { + done.countDown(); + } + }); + } + + start.countDown(); + assertTrue(done.await(60, TimeUnit.SECONDS), "concurrent increment did not finish in time"); + executor.shutdown(); + + assertEquals(0, errors.get(), () -> "errors during concurrent increment: " + errorList); + + long expected = (long) threadCount * incrementsPerThread; + TTSCountData server = repo.getServerTTSCount(botId, serverId, date); + TTSCountData global = repo.getGlobalTTSCount(botId, date); + assertEquals(expected, server.getCharCount(), "server char count lost writes"); + assertEquals(expected, server.getMessageCount(), "server message count lost writes"); + assertEquals(expected, global.getCharCount(), "global char count lost writes"); + assertEquals(expected, global.getMessageCount(), "global message count lost writes"); + + repo.dispose(); + } +} diff --git a/selfhost/src/main/java/dev/felnull/itts/config/ConfigImpl.java b/selfhost/src/main/java/dev/felnull/itts/config/ConfigImpl.java index 704dd5f..0967ec6 100644 --- a/selfhost/src/main/java/dev/felnull/itts/config/ConfigImpl.java +++ b/selfhost/src/main/java/dev/felnull/itts/config/ConfigImpl.java @@ -5,6 +5,7 @@ import dev.felnull.itts.config.old.ConfigV0; import dev.felnull.itts.core.config.Config; import dev.felnull.itts.core.config.DataBaseConfig; +import dev.felnull.itts.core.config.MetricsConfig; import dev.felnull.itts.core.config.voicetype.VoiceTextConfig; import dev.felnull.itts.core.config.voicetype.VoicevoxConfig; import dev.felnull.itts.core.util.NameSerializableEnum; @@ -26,6 +27,7 @@ * @param coeirolnkConfig COEIROLNK コンフィグ * @param sharevoxConfig SHAREVOX コンフィグ * @param dataBaseConfig データベースコンフィグ + * @param metricsConfig Prometheusメトリクスコンフィグ */ public record ConfigImpl( String botToken, @@ -35,7 +37,8 @@ public record ConfigImpl( VoicevoxConfig voicevoxConfig, VoicevoxConfig coeirolnkConfig, VoicevoxConfig sharevoxConfig, - DataBaseConfig dataBaseConfig + DataBaseConfig dataBaseConfig, + MetricsConfig metricsConfig ) implements Config { /** @@ -52,6 +55,7 @@ public ConfigImpl load(JsonObject json5) { VoicevoxConfig coeirolnkConfig = VoicevoxConfigImpl.fromJson(Optional.ofNullable(json5.getObject("coeirolnk")).orElseGet(JsonObject::new)); VoicevoxConfig sharevoxConfig = VoicevoxConfigImpl.fromJson(Optional.ofNullable(json5.getObject("sharevox")).orElseGet(JsonObject::new)); DataBaseConfig dataBaseConfig = DataBaseConfigImpl.fromJson(Optional.ofNullable(json5.getObject("data_base")).orElseGet(JsonObject::new)); + MetricsConfig metricsConfig = MetricsConfigImpl.fromJson(Optional.ofNullable(json5.getObject("metrics")).orElseGet(JsonObject::new)); return new ConfigImpl( botToken, @@ -61,7 +65,8 @@ public ConfigImpl load(JsonObject json5) { voicevoxConfig, coeirolnkConfig, sharevoxConfig, - dataBaseConfig + dataBaseConfig, + metricsConfig ); } @@ -77,7 +82,8 @@ public ConfigImpl migrate(Object oldConfig) { new VoicevoxConfigImpl(configV0.voicevoxConfig().enable(), configV0.voicevoxConfig().apiUrls(), configV0.voicevoxConfig().checkTime()), new VoicevoxConfigImpl(configV0.coeirolnkConfig().enable(), configV0.coeirolnkConfig().apiUrls(), configV0.coeirolnkConfig().checkTime()), new VoicevoxConfigImpl(configV0.sharevoxConfig().enable(), configV0.sharevoxConfig().apiUrls(), configV0.sharevoxConfig().checkTime()), - new DataBaseConfigImpl() + new DataBaseConfigImpl(), + new MetricsConfigImpl() ); } }; @@ -96,7 +102,8 @@ public static ConfigImpl createInitialConfig() { new VoicevoxConfigImpl(), new VoicevoxConfigImpl(), new VoicevoxConfigImpl(), - new DataBaseConfigImpl() + new DataBaseConfigImpl(), + new MetricsConfigImpl() ); } @@ -112,6 +119,7 @@ public void writeToJson(JsonObject json5) { json5.put("coeirolnk", ((VoicevoxConfigImpl) this.coeirolnkConfig).toJson(), "COEIROLNKのコンフィグ"); json5.put("sharevox", ((VoicevoxConfigImpl) this.sharevoxConfig).toJson(), "SHAREVOXのコンフィグ"); json5.put("data_base", ((DataBaseConfigImpl) this.dataBaseConfig).toJson(), "データベースのコンフィグ"); + json5.put("metrics", ((MetricsConfigImpl) this.metricsConfig).toJson(), "Prometheusメトリクスのコンフィグ"); } @Override @@ -154,6 +162,11 @@ public DataBaseConfig getDataBaseConfig() { return dataBaseConfig; } + @Override + public MetricsConfig getMetricsConfig() { + return metricsConfig; + } + /** * VOICETEXTコンフィグの実装 * @@ -313,4 +326,52 @@ public JsonObject toJson() { return password; } } + + /** + * Prometheusメトリクスコンフィグの実装 + * + * @param enabled 有効かどうか + * @param bindAddress バインドアドレス + * @param port ポート番号 + */ + private record MetricsConfigImpl( + boolean enabled, + String bindAddress, + @Range(from = 0, to = 65535) int port + ) implements MetricsConfig { + + private MetricsConfigImpl() { + this(DEFAULT_ENABLED, DEFAULT_BIND_ADDRESS, DEFAULT_PORT); + } + + public static MetricsConfigImpl fromJson(JsonObject jo) { + boolean enabled = jo.getBoolean("enable", DEFAULT_ENABLED); + String bindAddress = Json5Utils.getStringOrElse(jo, "bind_address", DEFAULT_BIND_ADDRESS); + int port = jo.getInt("port", DEFAULT_PORT); + return new MetricsConfigImpl(enabled, bindAddress, port); + } + + public JsonObject toJson() { + JsonObject jo = new JsonObject(); + jo.put("enable", JsonPrimitive.of(enabled), "Prometheusメトリクス公開を有効にするかどうか"); + jo.put("bind_address", JsonPrimitive.of(bindAddress), "メトリクスHTTPサーバーのバインドアドレス"); + jo.put("port", new JsonPrimitive((long) port), "メトリクスHTTPサーバーのポート番号"); + return jo; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public @NotNull String getBindAddress() { + return bindAddress; + } + + @Override + public @Range(from = 0, to = 65535) int getPort() { + return port; + } + } } From 0539c28d31545597278a95893d25851b71d59342 Mon Sep 17 00:00:00 2001 From: yuu1111 Date: Fri, 8 May 2026 06:00:23 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E6=8C=87=E6=91=98=E3=81=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - server_id = 0 をBot全体合計の予約値とし、server_id IS NULLの分岐を削除 unique制約とON CONFLICT/ON DUPLICATE KEY UPDATE単一クエリに統一しrace conditionを解消 - /stat week でメッセージ数も表示 - TTSCountData#getRecord を追加し、StatCommand での2重SELECTを解消 - 起動時のCounter累計注入を削除 (Prometheusのrate計算を狂わせるため) - MetricsRegistry の getOrCreate{Char,Message}Counter を共通化 - DataRepositoryImpl の sum 系ボイラープレートをwithConnectionヘルパで集約 - botId解決を getBot().getBotId() に統一 - RELATIVE_TIME_FORMAT を BaseCommand に集約 - PrometheusHttpExposer に固定スレッドプールを設定し詰まりを回避 --- .../dev/felnull/itts/core/ITTSRuntime.java | 27 ---- .../itts/core/audio/VoiceAudioScheduler.java | 2 +- .../core/discord/command/BaseCommand.java | 5 + .../core/discord/command/InfoCommand.java | 5 - .../core/discord/command/StatCommand.java | 25 ++-- .../itts/core/metrics/MetricsRegistry.java | 68 +++++++-- .../core/metrics/PrometheusHttpExposer.java | 39 ++++- .../felnull/itts/core/savedata/dao/DAO.java | 46 ++++-- .../core/savedata/dao/TTSCountRecord.java | 6 +- .../itts/core/savedata/dao/impl/MySQLDAO.java | 137 +++++------------- .../core/savedata/dao/impl/SQLiteDAO.java | 137 +++++------------- .../savedata/repository/DataRepository.java | 21 +++ .../savedata/repository/TTSCountData.java | 19 ++- .../repository/impl/DataRepositoryImpl.java | 115 +++++++++------ .../repository/impl/TTSCountDataImpl.java | 35 ++--- .../itts/core/tts/TTSCountRecorder.java | 4 +- 16 files changed, 349 insertions(+), 342 deletions(-) diff --git a/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java b/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java index 2e709fa..ae2ce1e 100644 --- a/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java +++ b/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java @@ -10,7 +10,6 @@ import dev.felnull.itts.core.metrics.MetricsRegistry; import dev.felnull.itts.core.metrics.PrometheusHttpExposer; import dev.felnull.itts.core.savedata.SaveDataManager; -import dev.felnull.itts.core.savedata.repository.DataRepository; import dev.felnull.itts.core.tts.TTSCountRecorder; import dev.felnull.itts.core.tts.TTSManager; import dev.felnull.itts.core.voice.VoiceManager; @@ -241,7 +240,6 @@ private void initMetrics() { this.prometheusHttpExposer = new PrometheusHttpExposer(metricsRegistry); this.prometheusHttpExposer.start(metricsConfig.getBindAddress(), metricsConfig.getPort()); logger.info("Prometheus metrics endpoint started on {}:{}/metrics", metricsConfig.getBindAddress(), metricsConfig.getPort()); - warmupMetricsCounters(); } catch (Exception e) { logger.warn("Failed to start Prometheus HTTP exposer", e); this.prometheusHttpExposer = null; @@ -254,31 +252,6 @@ private void initMetrics() { }, "prometheus-exposer-shutdown")); } - private void warmupMetricsCounters() { - if (metricsRegistry == null) { - return; - } - - DataRepository repo = SaveDataManager.getInstance().getRepository(); - if (repo == null) { - return; - } - - try { - long botId = bot.getBotId(); - long charTotal = repo.sumGlobalAllCharCount(botId); - long messageTotal = repo.sumGlobalAllMessageCount(botId); - if (charTotal > 0) { - metricsRegistry.getOrCreateCharCounter(botId, null).increment(charTotal); - } - if (messageTotal > 0) { - metricsRegistry.getOrCreateMessageCounter(botId, null).increment(messageTotal); - } - } catch (Throwable t) { - logger.warn("Failed to warmup metrics counters", t); - } - } - public long getStartupTime() { return startupTime; } diff --git a/core/src/main/java/dev/felnull/itts/core/audio/VoiceAudioScheduler.java b/core/src/main/java/dev/felnull/itts/core/audio/VoiceAudioScheduler.java index a7512a7..8ca59da 100644 --- a/core/src/main/java/dev/felnull/itts/core/audio/VoiceAudioScheduler.java +++ b/core/src/main/java/dev/felnull/itts/core/audio/VoiceAudioScheduler.java @@ -100,7 +100,7 @@ public CompletableFuture load(SaidText saidText) { TTSCountRecorder recorder = ITTSRuntime.getInstance().getTTSCountRecorder(); if (recorder != null && finalText != null) { - long botId = audioManager.getGuild().getJDA().getSelfUser().getIdLong(); + long botId = getBot().getBotId(); recorder.record(botId, guildId, finalText.length()); } diff --git a/core/src/main/java/dev/felnull/itts/core/discord/command/BaseCommand.java b/core/src/main/java/dev/felnull/itts/core/discord/command/BaseCommand.java index 993ad67..ae7dc18 100644 --- a/core/src/main/java/dev/felnull/itts/core/discord/command/BaseCommand.java +++ b/core/src/main/java/dev/felnull/itts/core/discord/command/BaseCommand.java @@ -24,6 +24,11 @@ public abstract class BaseCommand implements ITTSRuntimeUse { */ protected static final DefaultMemberPermissions OWNERS_PERMISSIONS = DefaultMemberPermissions.enabledFor(Permission.MANAGE_SERVER); + /** + * 時間を相対的に表示するフォーマット + */ + protected static final String RELATIVE_TIME_FORMAT = ""; + /** * コマンド名 */ diff --git a/core/src/main/java/dev/felnull/itts/core/discord/command/InfoCommand.java b/core/src/main/java/dev/felnull/itts/core/discord/command/InfoCommand.java index d6cd5c5..737acc0 100644 --- a/core/src/main/java/dev/felnull/itts/core/discord/command/InfoCommand.java +++ b/core/src/main/java/dev/felnull/itts/core/discord/command/InfoCommand.java @@ -28,11 +28,6 @@ public class InfoCommand extends BaseCommand { */ private static final String SOURCE_URL = "https://github.com/TeamFelnull/I-TTS"; - /** - * 時間を相対的に表示するフォーマット - */ - private static final String RELATIVE_TIME_FORMAT = ""; - /** * コンストラクタ */ diff --git a/core/src/main/java/dev/felnull/itts/core/discord/command/StatCommand.java b/core/src/main/java/dev/felnull/itts/core/discord/command/StatCommand.java index 7e8b114..c993688 100644 --- a/core/src/main/java/dev/felnull/itts/core/discord/command/StatCommand.java +++ b/core/src/main/java/dev/felnull/itts/core/discord/command/StatCommand.java @@ -1,6 +1,7 @@ package dev.felnull.itts.core.discord.command; import dev.felnull.itts.core.savedata.SaveDataManager; +import dev.felnull.itts.core.savedata.dao.TTSCountRecord; import dev.felnull.itts.core.savedata.repository.DataRepository; import dev.felnull.itts.core.savedata.repository.TTSCountData; import net.dv8tion.jda.api.EmbedBuilder; @@ -16,17 +17,13 @@ import java.time.LocalDate; import java.time.ZoneOffset; import java.util.Objects; +import java.util.Optional; /** * 読み上げ統計表示コマンド */ public class StatCommand extends BaseCommand { - /** - * 時間を相対的に表示するフォーマット - */ - private static final String RELATIVE_TIME_FORMAT = ""; - /** * コンストラクタ */ @@ -60,13 +57,17 @@ public void commandInteraction(SlashCommandInteractionEvent event) { private void today(SlashCommandInteractionEvent event) { DataRepository repo = SaveDataManager.getInstance().getRepository(); - long botId = event.getJDA().getSelfUser().getIdLong(); + long botId = getBot().getBotId(); LocalDate today = LocalDate.now(ZoneOffset.UTC); TTSCountData data = repo.getGlobalTTSCount(botId, today); + Optional record = data.getRecord(); + long charCount = record.map(TTSCountRecord::charCount).orElse(0L); + long messageCount = record.map(TTSCountRecord::messageCount).orElse(0L); + EmbedBuilder builder = baseEmbed("本日の読み上げ統計 (UTC)"); - builder.addField("文字数", data.getCharCount() + "文字", true); - builder.addField("メッセージ数", data.getMessageCount() + "件", true); + builder.addField("文字数", charCount + "文字", true); + builder.addField("メッセージ数", messageCount + "件", true); addUptimeFields(builder); event.replyEmbeds(builder.build()).setEphemeral(true).queue(); @@ -74,15 +75,17 @@ private void today(SlashCommandInteractionEvent event) { private void week(SlashCommandInteractionEvent event) { DataRepository repo = SaveDataManager.getInstance().getRepository(); - long botId = event.getJDA().getSelfUser().getIdLong(); + long botId = getBot().getBotId(); LocalDate today = LocalDate.now(ZoneOffset.UTC); LocalDate from = today.minusDays(6); long charSum = repo.sumGlobalCharCount(botId, from, today); + long messageSum = repo.sumGlobalMessageCount(botId, from, today); EmbedBuilder builder = baseEmbed("過去7日の読み上げ統計 (UTC)"); builder.addField("期間", from + " - " + today, false); builder.addField("文字数", charSum + "文字", true); + builder.addField("メッセージ数", messageSum + "件", true); addUptimeFields(builder); event.replyEmbeds(builder.build()).setEphemeral(true).queue(); @@ -90,7 +93,7 @@ private void week(SlashCommandInteractionEvent event) { private void all(SlashCommandInteractionEvent event) { DataRepository repo = SaveDataManager.getInstance().getRepository(); - long botId = event.getJDA().getSelfUser().getIdLong(); + long botId = getBot().getBotId(); long charSum = repo.sumGlobalAllCharCount(botId); long messageSum = repo.sumGlobalAllMessageCount(botId); @@ -106,7 +109,7 @@ private void all(SlashCommandInteractionEvent event) { private void server(SlashCommandInteractionEvent event) { Guild guild = Objects.requireNonNull(event.getGuild()); DataRepository repo = SaveDataManager.getInstance().getRepository(); - long botId = event.getJDA().getSelfUser().getIdLong(); + long botId = getBot().getBotId(); long serverId = guild.getIdLong(); long charSum = repo.sumServerAllCharCount(botId, serverId); diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java b/core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java index 011a58b..2b80b0b 100644 --- a/core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java +++ b/core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java @@ -17,6 +17,31 @@ */ public final class MetricsRegistry { + /** + * BOT全体合計を表すサーバーIDの予約値 + */ + public static final long GLOBAL_SERVER_ID = 0L; + + /** + * 文字数Counterのメトリクス名 + */ + private static final String CHAR_METRIC_NAME = "itts_spoken_chars_total"; + + /** + * 文字数Counterの説明 + */ + private static final String CHAR_METRIC_DESCRIPTION = "Total spoken characters delivered to TTS API"; + + /** + * メッセージ数Counterのメトリクス名 + */ + private static final String MESSAGE_METRIC_NAME = "itts_spoken_messages_total"; + + /** + * メッセージ数Counterの説明 + */ + private static final String MESSAGE_METRIC_DESCRIPTION = "Total spoken messages delivered to TTS API"; + /** * Prometheus形式のレジストリ */ @@ -71,33 +96,48 @@ public PrometheusMeterRegistry getRegistry() { * 文字数Counterをラベル付きで取得 * * @param botId BOTのID - * @param serverId サーバーID nullの場合はBOT全体 + * @param serverId サーバーID GLOBAL_SERVER_IDの場合はBOT全体 * @return Counter */ @NotNull - public Counter getOrCreateCharCounter(long botId, Long serverId) { - String key = botId + "|" + (serverId == null ? "global" : serverId.toString()); - return charCounters.computeIfAbsent(key, k -> Counter.builder("itts_spoken_chars_total") - .description("Total spoken characters delivered to TTS API") - .tag("bot_id", String.valueOf(botId)) - .tag("server_id", serverId == null ? "global" : String.valueOf(serverId)) - .register(registry)); + public Counter getOrCreateCharCounter(long botId, long serverId) { + return getOrCreate(charCounters, CHAR_METRIC_NAME, CHAR_METRIC_DESCRIPTION, botId, serverId); } /** * メッセージ数Counterをラベル付きで取得 * * @param botId BOTのID - * @param serverId サーバーID nullの場合はBOT全体 + * @param serverId サーバーID GLOBAL_SERVER_IDの場合はBOT全体 + * @return Counter + */ + @NotNull + public Counter getOrCreateMessageCounter(long botId, long serverId) { + return getOrCreate(messageCounters, MESSAGE_METRIC_NAME, MESSAGE_METRIC_DESCRIPTION, botId, serverId); + } + + /** + * Counterをキャッシュから取得もしくは生成する + * + * @param cache Counterキャッシュ + * @param metricName メトリクス名 + * @param description メトリクスの説明 + * @param botId BOTのID + * @param serverId サーバーID GLOBAL_SERVER_IDの場合はBOT全体 * @return Counter */ @NotNull - public Counter getOrCreateMessageCounter(long botId, Long serverId) { - String key = botId + "|" + (serverId == null ? "global" : serverId.toString()); - return messageCounters.computeIfAbsent(key, k -> Counter.builder("itts_spoken_messages_total") - .description("Total spoken messages delivered to TTS API") + private Counter getOrCreate(@NotNull ConcurrentMap cache, + @NotNull String metricName, + @NotNull String description, + long botId, + long serverId) { + String serverTag = serverId == GLOBAL_SERVER_ID ? "global" : String.valueOf(serverId); + String key = botId + "|" + serverTag; + return cache.computeIfAbsent(key, k -> Counter.builder(metricName) + .description(description) .tag("bot_id", String.valueOf(botId)) - .tag("server_id", serverId == null ? "global" : String.valueOf(serverId)) + .tag("server_id", serverTag) .register(registry)); } } diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java b/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java index 71590dc..46b2574 100644 --- a/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java +++ b/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java @@ -1,18 +1,32 @@ package dev.felnull.itts.core.metrics; import com.sun.net.httpserver.HttpServer; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; /** * PrometheusメトリクスをHTTPで公開するエクスポーザ */ public final class PrometheusHttpExposer { + /** + * リクエスト処理スレッドプールのスレッド数 + */ + private static final int EXECUTOR_THREAD_COUNT = 2; + + /** + * Executor停止時の待機秒数 + */ + private static final int EXECUTOR_SHUTDOWN_TIMEOUT_SEC = 5; + /** * メトリクスレジストリ */ @@ -23,6 +37,11 @@ public final class PrometheusHttpExposer { */ private HttpServer httpServer; + /** + * リクエスト処理用スレッドプール + */ + private ExecutorService executor; + /** * コンストラクタ * @@ -50,9 +69,15 @@ public void start(@NotNull String host, int port) throws IOException { out.write(bytes); } }); - server.setExecutor(null); + ExecutorService pool = Executors.newFixedThreadPool(EXECUTOR_THREAD_COUNT, + new BasicThreadFactory.Builder() + .namingPattern("prometheus-exposer-%d") + .daemon(true) + .build()); + server.setExecutor(pool); server.start(); this.httpServer = server; + this.executor = pool; } /** @@ -63,5 +88,17 @@ public void stop() { httpServer.stop(0); httpServer = null; } + if (executor != null) { + executor.shutdown(); + try { + if (!executor.awaitTermination(EXECUTOR_SHUTDOWN_TIMEOUT_SEC, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + executor = null; + } } } diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/dao/DAO.java b/core/src/main/java/dev/felnull/itts/core/savedata/dao/DAO.java index c60c610..ef41315 100644 --- a/core/src/main/java/dev/felnull/itts/core/savedata/dao/DAO.java +++ b/core/src/main/java/dev/felnull/itts/core/savedata/dao/DAO.java @@ -776,16 +776,21 @@ interface GlobalCustomDictionaryTable extends Table { } /** - * 読み上げ文字数集計テーブル + * 期間内の文字数合計を取得する */ interface TTSCountTable extends Table { + /** + * BOT全体合計を表すサーバーキーIDの予約値 + */ + int GLOBAL_SERVER_KEY_ID = 0; + /** * 指定日のカウントを増分する * * @param connection コネクション * @param botKeyId BOTキーID - * @param serverKeyId サーバーキーID nullの場合はBOT全体合計 + * @param serverKeyId サーバーキーID GLOBAL_SERVER_KEY_IDの場合はBOT全体合計 * @param date 集計日 * @param charDelta 文字数の増分 * @param messageDelta メッセージ数の増分 @@ -793,7 +798,7 @@ interface TTSCountTable extends Table { */ void incrementCount(@NotNull Connection connection, int botKeyId, - @Nullable Integer serverKeyId, + int serverKeyId, @NotNull LocalDate date, long charDelta, long messageDelta) throws SQLException; @@ -803,14 +808,14 @@ void incrementCount(@NotNull Connection connection, * * @param connection コネクション * @param botKeyId BOTキーID - * @param serverKeyId サーバーキーID nullの場合はBOT全体合計 + * @param serverKeyId サーバーキーID GLOBAL_SERVER_KEY_IDの場合はBOT全体合計 * @param date 集計日 * @return カウントレコード * @throws SQLException エラー */ Optional getCount(@NotNull Connection connection, int botKeyId, - @Nullable Integer serverKeyId, + int serverKeyId, @NotNull LocalDate date) throws SQLException; /** @@ -818,43 +823,60 @@ Optional getCount(@NotNull Connection connection, * * @param connection コネクション * @param botKeyId BOTキーID - * @param serverKeyId サーバーキーID nullの場合はBOT全体合計 + * @param serverKeyId サーバーキーID GLOBAL_SERVER_KEY_IDの場合はBOT全体合計 * @param from 開始日 (含む) * @param to 終了日 (含む) - * @return 文字数とメッセージ数の合計 + * @return 文字数の合計 * @throws SQLException エラー */ long sumCharCountRange(@NotNull Connection connection, int botKeyId, - @Nullable Integer serverKeyId, + int serverKeyId, @NotNull LocalDate from, @NotNull LocalDate to) throws SQLException; + /** + * 期間内のメッセージ数合計を取得する + * + * @param connection コネクション + * @param botKeyId BOTキーID + * @param serverKeyId サーバーキーID GLOBAL_SERVER_KEY_IDの場合はBOT全体合計 + * @param from 開始日 (含む) + * @param to 終了日 (含む) + * @return メッセージ数の合計 + * @throws SQLException エラー + */ + long sumMessageCountRange(@NotNull Connection connection, + int botKeyId, + int serverKeyId, + @NotNull LocalDate from, + @NotNull LocalDate to) throws SQLException; + /** * 全期間の文字数合計を取得する * * @param connection コネクション * @param botKeyId BOTキーID - * @param serverKeyId サーバーキーID nullの場合はBOT全体合計 + * @param serverKeyId サーバーキーID GLOBAL_SERVER_KEY_IDの場合はBOT全体合計 * @return 文字数の合計 * @throws SQLException エラー */ long sumAllCharCount(@NotNull Connection connection, int botKeyId, - @Nullable Integer serverKeyId) throws SQLException; + int serverKeyId) throws SQLException; /** * 全期間のメッセージ数合計を取得する * * @param connection コネクション * @param botKeyId BOTキーID - * @param serverKeyId サーバーキーID nullの場合はBOT全体合計 + * @param serverKeyId サーバーキーID GLOBAL_SERVER_KEY_IDの場合はBOT全体合計 * @return メッセージ数の合計 * @throws SQLException エラー */ long sumAllMessageCount(@NotNull Connection connection, int botKeyId, - @Nullable Integer serverKeyId) throws SQLException; + int serverKeyId) throws SQLException; } } diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/dao/TTSCountRecord.java b/core/src/main/java/dev/felnull/itts/core/savedata/dao/TTSCountRecord.java index 1d6d35d..8ebd0a5 100644 --- a/core/src/main/java/dev/felnull/itts/core/savedata/dao/TTSCountRecord.java +++ b/core/src/main/java/dev/felnull/itts/core/savedata/dao/TTSCountRecord.java @@ -1,20 +1,18 @@ package dev.felnull.itts.core.savedata.dao; -import org.jetbrains.annotations.Nullable; - import java.time.LocalDate; /** * 読み上げ文字数集計のレコード * * @param botId BOTのDiscord ID - * @param serverId サーバーのDiscord ID nullの場合はBOT全体合計 + * @param serverId サーバーのDiscord ID 0の場合はBOT全体合計 * @param date 集計日 * @param charCount 読み上げ文字数 * @param messageCount 読み上げメッセージ数 */ public record TTSCountRecord(long botId, - @Nullable Long serverId, + long serverId, LocalDate date, long charCount, long messageCount diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/dao/impl/MySQLDAO.java b/core/src/main/java/dev/felnull/itts/core/savedata/dao/impl/MySQLDAO.java index 01899bd..20de614 100644 --- a/core/src/main/java/dev/felnull/itts/core/savedata/dao/impl/MySQLDAO.java +++ b/core/src/main/java/dev/felnull/itts/core/savedata/dao/impl/MySQLDAO.java @@ -2471,14 +2471,13 @@ public void createTableIfNotExists(@NotNull Connection connection) throws SQLExc create table if not exists tts_count_data( id integer not null primary key auto_increment, bot_id integer not null, - server_id integer, + server_id integer not null, target_date date not null, spoken_char_count bigint not null default 0, spoken_message_count bigint not null default 0, unique(bot_id, server_id, target_date), - foreign key (bot_id) references bot_key(id), - foreign key (server_id) references server_key(id) + foreign key (bot_id) references bot_key(id) ); """; @@ -2488,15 +2487,10 @@ create table if not exists tts_count_data( @Override public void incrementCount(@NotNull Connection connection, int botKeyId, - @Nullable Integer serverKeyId, + int serverKeyId, @NotNull LocalDate date, long charDelta, long messageDelta) throws SQLException { - if (serverKeyId == null) { - incrementCountServerNull(connection, botKeyId, date, charDelta, messageDelta); - return; - } - @Language("MySQL") String sql = """ insert into tts_count_data(bot_id, server_id, target_date, spoken_char_count, spoken_message_count) @@ -2516,80 +2510,29 @@ insert into tts_count_data(bot_id, server_id, target_date, spoken_char_count, sp } } - private void incrementCountServerNull(@NotNull Connection connection, - int botKeyId, - @NotNull LocalDate date, - long charDelta, - long messageDelta) throws SQLException { - @Language("MySQL") - String updateSql = """ - update tts_count_data - set spoken_char_count = spoken_char_count + ?, - spoken_message_count = spoken_message_count + ? - where bot_id = ? and server_id is null and target_date = ? - """; - - try (PreparedStatement statement = connection.prepareStatement(updateSql)) { - statement.setLong(1, charDelta); - statement.setLong(2, messageDelta); - statement.setInt(3, botKeyId); - statement.setString(4, date.toString()); - - if (statement.executeUpdate() > 0) { - return; - } - } - - @Language("MySQL") - String insertSql = """ - insert into tts_count_data(bot_id, server_id, target_date, spoken_char_count, spoken_message_count) - values (?, null, ?, ?, ?) - """; - - try (PreparedStatement statement = connection.prepareStatement(insertSql)) { - statement.setInt(1, botKeyId); - statement.setString(2, date.toString()); - statement.setLong(3, charDelta); - statement.setLong(4, messageDelta); - statement.execute(); - } - } - @Override public Optional getCount(@NotNull Connection connection, int botKeyId, - @Nullable Integer serverKeyId, + int serverKeyId, @NotNull LocalDate date) throws SQLException { @Language("MySQL") - String sqlServer = """ + String sql = """ select spoken_char_count, spoken_message_count from tts_count_data where bot_id = ? and server_id = ? and target_date = ? limit 1 """; - @Language("MySQL") - String sqlNull = """ - select spoken_char_count, spoken_message_count - from tts_count_data - where bot_id = ? and server_id is null and target_date = ? - limit 1 - """; - - try (PreparedStatement statement = connection.prepareStatement(serverKeyId == null ? sqlNull : sqlServer)) { + try (PreparedStatement statement = connection.prepareStatement(sql)) { statement.setInt(1, botKeyId); - if (serverKeyId == null) { - statement.setString(2, date.toString()); - } else { - statement.setInt(2, serverKeyId); - statement.setString(3, date.toString()); - } + statement.setInt(2, serverKeyId); + statement.setString(3, date.toString()); try (ResultSet rs = statement.executeQuery()) { if (rs.next()) { long ch = rs.getLong("spoken_char_count"); long ms = rs.getLong("spoken_message_count"); - return Optional.of(new TTSCountRecord(0L, null, date, ch, ms)); + return Optional.of(new TTSCountRecord(0L, 0L, date, ch, ms)); } } } @@ -2600,33 +2543,36 @@ public Optional getCount(@NotNull Connection connection, @Override public long sumCharCountRange(@NotNull Connection connection, int botKeyId, - @Nullable Integer serverKeyId, + int serverKeyId, @NotNull LocalDate from, @NotNull LocalDate to) throws SQLException { - @Language("MySQL") - String sqlServer = """ - select coalesce(sum(spoken_char_count), 0) as total - from tts_count_data - where bot_id = ? and server_id = ? and target_date between ? and ? - """; + return sumRangeByColumn(connection, botKeyId, serverKeyId, from, to, "spoken_char_count"); + } + + @Override + public long sumMessageCountRange(@NotNull Connection connection, + int botKeyId, + int serverKeyId, + @NotNull LocalDate from, + @NotNull LocalDate to) throws SQLException { + return sumRangeByColumn(connection, botKeyId, serverKeyId, from, to, "spoken_message_count"); + } + private long sumRangeByColumn(@NotNull Connection connection, + int botKeyId, + int serverKeyId, + @NotNull LocalDate from, + @NotNull LocalDate to, + @NotNull String column) throws SQLException { @Language("MySQL") - String sqlNull = """ - select coalesce(sum(spoken_char_count), 0) as total - from tts_count_data - where bot_id = ? and server_id is null and target_date between ? and ? - """; + String sql = "select coalesce(sum(" + column + "), 0) as total from tts_count_data " + + "where bot_id = ? and server_id = ? and target_date between ? and ?"; - try (PreparedStatement statement = connection.prepareStatement(serverKeyId == null ? sqlNull : sqlServer)) { + try (PreparedStatement statement = connection.prepareStatement(sql)) { statement.setInt(1, botKeyId); - if (serverKeyId == null) { - statement.setString(2, from.toString()); - statement.setString(3, to.toString()); - } else { - statement.setInt(2, serverKeyId); - statement.setString(3, from.toString()); - statement.setString(4, to.toString()); - } + statement.setInt(2, serverKeyId); + statement.setString(3, from.toString()); + statement.setString(4, to.toString()); try (ResultSet rs = statement.executeQuery()) { if (rs.next()) { @@ -2641,32 +2587,27 @@ select coalesce(sum(spoken_char_count), 0) as total @Override public long sumAllCharCount(@NotNull Connection connection, int botKeyId, - @Nullable Integer serverKeyId) throws SQLException { + int serverKeyId) throws SQLException { return sumAllByColumn(connection, botKeyId, serverKeyId, "spoken_char_count"); } @Override public long sumAllMessageCount(@NotNull Connection connection, int botKeyId, - @Nullable Integer serverKeyId) throws SQLException { + int serverKeyId) throws SQLException { return sumAllByColumn(connection, botKeyId, serverKeyId, "spoken_message_count"); } private long sumAllByColumn(@NotNull Connection connection, int botKeyId, - @Nullable Integer serverKeyId, + int serverKeyId, @NotNull String column) throws SQLException { @Language("MySQL") - String sqlServer = "select coalesce(sum(" + column + "), 0) as total from tts_count_data where bot_id = ? and server_id = ?"; - - @Language("MySQL") - String sqlNull = "select coalesce(sum(" + column + "), 0) as total from tts_count_data where bot_id = ? and server_id is null"; + String sql = "select coalesce(sum(" + column + "), 0) as total from tts_count_data where bot_id = ? and server_id = ?"; - try (PreparedStatement statement = connection.prepareStatement(serverKeyId == null ? sqlNull : sqlServer)) { + try (PreparedStatement statement = connection.prepareStatement(sql)) { statement.setInt(1, botKeyId); - if (serverKeyId != null) { - statement.setInt(2, serverKeyId); - } + statement.setInt(2, serverKeyId); try (ResultSet rs = statement.executeQuery()) { if (rs.next()) { diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/dao/impl/SQLiteDAO.java b/core/src/main/java/dev/felnull/itts/core/savedata/dao/impl/SQLiteDAO.java index ebdad8b..c9bb77d 100644 --- a/core/src/main/java/dev/felnull/itts/core/savedata/dao/impl/SQLiteDAO.java +++ b/core/src/main/java/dev/felnull/itts/core/savedata/dao/impl/SQLiteDAO.java @@ -2457,14 +2457,13 @@ public void createTableIfNotExists(@NotNull Connection connection) throws SQLExc create table if not exists tts_count_data( id integer not null primary key autoincrement, bot_id integer not null, - server_id integer, + server_id integer not null, target_date date not null, spoken_char_count bigint not null default 0, spoken_message_count bigint not null default 0, unique(bot_id, server_id, target_date), - foreign key (bot_id) references bot_key(id), - foreign key (server_id) references server_key(id) + foreign key (bot_id) references bot_key(id) ); """; @@ -2474,15 +2473,10 @@ create table if not exists tts_count_data( @Override public void incrementCount(@NotNull Connection connection, int botKeyId, - @Nullable Integer serverKeyId, + int serverKeyId, @NotNull LocalDate date, long charDelta, long messageDelta) throws SQLException { - if (serverKeyId == null) { - incrementCountServerNull(connection, botKeyId, date, charDelta, messageDelta); - return; - } - @Language("SQLite") String sql = """ insert into tts_count_data(bot_id, server_id, target_date, spoken_char_count, spoken_message_count) @@ -2502,80 +2496,29 @@ on conflict(bot_id, server_id, target_date) do update set } } - private void incrementCountServerNull(@NotNull Connection connection, - int botKeyId, - @NotNull LocalDate date, - long charDelta, - long messageDelta) throws SQLException { - @Language("SQLite") - String updateSql = """ - update tts_count_data - set spoken_char_count = spoken_char_count + ?, - spoken_message_count = spoken_message_count + ? - where bot_id = ? and server_id is null and target_date = ? - """; - - try (PreparedStatement statement = connection.prepareStatement(updateSql)) { - statement.setLong(1, charDelta); - statement.setLong(2, messageDelta); - statement.setInt(3, botKeyId); - statement.setString(4, date.toString()); - - if (statement.executeUpdate() > 0) { - return; - } - } - - @Language("SQLite") - String insertSql = """ - insert into tts_count_data(bot_id, server_id, target_date, spoken_char_count, spoken_message_count) - values (?, null, ?, ?, ?) - """; - - try (PreparedStatement statement = connection.prepareStatement(insertSql)) { - statement.setInt(1, botKeyId); - statement.setString(2, date.toString()); - statement.setLong(3, charDelta); - statement.setLong(4, messageDelta); - statement.execute(); - } - } - @Override public Optional getCount(@NotNull Connection connection, int botKeyId, - @Nullable Integer serverKeyId, + int serverKeyId, @NotNull LocalDate date) throws SQLException { @Language("SQLite") - String sqlServer = """ + String sql = """ select spoken_char_count, spoken_message_count from tts_count_data where bot_id = ? and server_id = ? and target_date = ? limit 1 """; - @Language("SQLite") - String sqlNull = """ - select spoken_char_count, spoken_message_count - from tts_count_data - where bot_id = ? and server_id is null and target_date = ? - limit 1 - """; - - try (PreparedStatement statement = connection.prepareStatement(serverKeyId == null ? sqlNull : sqlServer)) { + try (PreparedStatement statement = connection.prepareStatement(sql)) { statement.setInt(1, botKeyId); - if (serverKeyId == null) { - statement.setString(2, date.toString()); - } else { - statement.setInt(2, serverKeyId); - statement.setString(3, date.toString()); - } + statement.setInt(2, serverKeyId); + statement.setString(3, date.toString()); try (ResultSet rs = statement.executeQuery()) { if (rs.next()) { long ch = rs.getLong("spoken_char_count"); long ms = rs.getLong("spoken_message_count"); - return Optional.of(new TTSCountRecord(0L, null, date, ch, ms)); + return Optional.of(new TTSCountRecord(0L, 0L, date, ch, ms)); } } } @@ -2586,33 +2529,36 @@ public Optional getCount(@NotNull Connection connection, @Override public long sumCharCountRange(@NotNull Connection connection, int botKeyId, - @Nullable Integer serverKeyId, + int serverKeyId, @NotNull LocalDate from, @NotNull LocalDate to) throws SQLException { - @Language("SQLite") - String sqlServer = """ - select coalesce(sum(spoken_char_count), 0) as total - from tts_count_data - where bot_id = ? and server_id = ? and target_date between ? and ? - """; + return sumRangeByColumn(connection, botKeyId, serverKeyId, from, to, "spoken_char_count"); + } + + @Override + public long sumMessageCountRange(@NotNull Connection connection, + int botKeyId, + int serverKeyId, + @NotNull LocalDate from, + @NotNull LocalDate to) throws SQLException { + return sumRangeByColumn(connection, botKeyId, serverKeyId, from, to, "spoken_message_count"); + } + private long sumRangeByColumn(@NotNull Connection connection, + int botKeyId, + int serverKeyId, + @NotNull LocalDate from, + @NotNull LocalDate to, + @NotNull String column) throws SQLException { @Language("SQLite") - String sqlNull = """ - select coalesce(sum(spoken_char_count), 0) as total - from tts_count_data - where bot_id = ? and server_id is null and target_date between ? and ? - """; + String sql = "select coalesce(sum(" + column + "), 0) as total from tts_count_data " + + "where bot_id = ? and server_id = ? and target_date between ? and ?"; - try (PreparedStatement statement = connection.prepareStatement(serverKeyId == null ? sqlNull : sqlServer)) { + try (PreparedStatement statement = connection.prepareStatement(sql)) { statement.setInt(1, botKeyId); - if (serverKeyId == null) { - statement.setString(2, from.toString()); - statement.setString(3, to.toString()); - } else { - statement.setInt(2, serverKeyId); - statement.setString(3, from.toString()); - statement.setString(4, to.toString()); - } + statement.setInt(2, serverKeyId); + statement.setString(3, from.toString()); + statement.setString(4, to.toString()); try (ResultSet rs = statement.executeQuery()) { if (rs.next()) { @@ -2627,32 +2573,27 @@ select coalesce(sum(spoken_char_count), 0) as total @Override public long sumAllCharCount(@NotNull Connection connection, int botKeyId, - @Nullable Integer serverKeyId) throws SQLException { + int serverKeyId) throws SQLException { return sumAllByColumn(connection, botKeyId, serverKeyId, "spoken_char_count"); } @Override public long sumAllMessageCount(@NotNull Connection connection, int botKeyId, - @Nullable Integer serverKeyId) throws SQLException { + int serverKeyId) throws SQLException { return sumAllByColumn(connection, botKeyId, serverKeyId, "spoken_message_count"); } private long sumAllByColumn(@NotNull Connection connection, int botKeyId, - @Nullable Integer serverKeyId, + int serverKeyId, @NotNull String column) throws SQLException { @Language("SQLite") - String sqlServer = "select coalesce(sum(" + column + "), 0) as total from tts_count_data where bot_id = ? and server_id = ?"; - - @Language("SQLite") - String sqlNull = "select coalesce(sum(" + column + "), 0) as total from tts_count_data where bot_id = ? and server_id is null"; + String sql = "select coalesce(sum(" + column + "), 0) as total from tts_count_data where bot_id = ? and server_id = ?"; - try (PreparedStatement statement = connection.prepareStatement(serverKeyId == null ? sqlNull : sqlServer)) { + try (PreparedStatement statement = connection.prepareStatement(sql)) { statement.setInt(1, botKeyId); - if (serverKeyId != null) { - statement.setInt(2, serverKeyId); - } + statement.setInt(2, serverKeyId); try (ResultSet rs = statement.executeQuery()) { if (rs.next()) { diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/repository/DataRepository.java b/core/src/main/java/dev/felnull/itts/core/savedata/repository/DataRepository.java index d454d4c..a0b51f6 100644 --- a/core/src/main/java/dev/felnull/itts/core/savedata/repository/DataRepository.java +++ b/core/src/main/java/dev/felnull/itts/core/savedata/repository/DataRepository.java @@ -180,6 +180,16 @@ static DataRepository create(DAO dao) { */ long sumGlobalCharCount(long botId, @NotNull LocalDate from, @NotNull LocalDate to); + /** + * BOT全体の期間内メッセージ数合計を取得 + * + * @param botId BOTのID + * @param from 開始日 + * @param to 終了日 + * @return メッセージ数合計 + */ + long sumGlobalMessageCount(long botId, @NotNull LocalDate from, @NotNull LocalDate to); + /** * サーバー単位の期間内文字数合計を取得 * @@ -191,6 +201,17 @@ static DataRepository create(DAO dao) { */ long sumServerCharCount(long botId, long serverId, @NotNull LocalDate from, @NotNull LocalDate to); + /** + * サーバー単位の期間内メッセージ数合計を取得 + * + * @param botId BOTのID + * @param serverId サーバーID + * @param from 開始日 + * @param to 終了日 + * @return メッセージ数合計 + */ + long sumServerMessageCount(long botId, long serverId, @NotNull LocalDate from, @NotNull LocalDate to); + /** * BOT全体の累計文字数を取得 * diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/repository/TTSCountData.java b/core/src/main/java/dev/felnull/itts/core/savedata/repository/TTSCountData.java index 1e74fad..3684203 100644 --- a/core/src/main/java/dev/felnull/itts/core/savedata/repository/TTSCountData.java +++ b/core/src/main/java/dev/felnull/itts/core/savedata/repository/TTSCountData.java @@ -1,5 +1,9 @@ package dev.felnull.itts.core.savedata.repository; +import dev.felnull.itts.core.savedata.dao.TTSCountRecord; + +import java.util.Optional; + /** * 読み上げ文字数集計データ */ @@ -13,17 +17,28 @@ public interface TTSCountData { */ void addCount(long charDelta, long messageDelta); + /** + * 該当日のレコードを取得 + * + * @return レコード 存在しない場合は空 + */ + Optional getRecord(); + /** * 文字数を取得 * * @return 文字数 */ - long getCharCount(); + default long getCharCount() { + return getRecord().map(TTSCountRecord::charCount).orElse(0L); + } /** * メッセージ数を取得 * * @return メッセージ数 */ - long getMessageCount(); + default long getMessageCount() { + return getRecord().map(TTSCountRecord::messageCount).orElse(0L); + } } diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/repository/impl/DataRepositoryImpl.java b/core/src/main/java/dev/felnull/itts/core/savedata/repository/impl/DataRepositoryImpl.java index 9733ea2..cdecff9 100644 --- a/core/src/main/java/dev/felnull/itts/core/savedata/repository/impl/DataRepositoryImpl.java +++ b/core/src/main/java/dev/felnull/itts/core/savedata/repository/impl/DataRepositoryImpl.java @@ -370,96 +370,117 @@ KeyData getVoiceTypeKeyData() { @Override public @NotNull TTSCountData getGlobalTTSCount(long botId, @NotNull LocalDate date) { - return new TTSCountDataImpl(this, botId, null, date); + return new TTSCountDataImpl(this, botId, 0L, date); } @Override public long sumGlobalCharCount(long botId, @NotNull LocalDate from, @NotNull LocalDate to) { - try (Connection connection = dao.getConnection()) { + return withConnection(connection -> { int botKeyId = botKeyData.getId(botId); - return dao.ttsCountTable().sumCharCountRange(connection, botKeyId, null, from, to); - } catch (Exception e) { - fireErrorEvent(e); - throw new RuntimeException(e); - } catch (Throwable throwable) { - fireErrorEvent(throwable); - throw throwable; - } + return dao.ttsCountTable().sumCharCountRange(connection, botKeyId, DAO.TTSCountTable.GLOBAL_SERVER_KEY_ID, from, to); + }, 0L); + } + + @Override + public long sumGlobalMessageCount(long botId, @NotNull LocalDate from, @NotNull LocalDate to) { + return withConnection(connection -> { + int botKeyId = botKeyData.getId(botId); + return dao.ttsCountTable().sumMessageCountRange(connection, botKeyId, DAO.TTSCountTable.GLOBAL_SERVER_KEY_ID, from, to); + }, 0L); } @Override public long sumServerCharCount(long botId, long serverId, @NotNull LocalDate from, @NotNull LocalDate to) { - try (Connection connection = dao.getConnection()) { + return withConnection(connection -> { int botKeyId = botKeyData.getId(botId); int serverKeyId = serverKeyData.getId(serverId); return dao.ttsCountTable().sumCharCountRange(connection, botKeyId, serverKeyId, from, to); - } catch (Exception e) { - fireErrorEvent(e); - throw new RuntimeException(e); - } catch (Throwable throwable) { - fireErrorEvent(throwable); - throw throwable; - } + }, 0L); + } + + @Override + public long sumServerMessageCount(long botId, long serverId, @NotNull LocalDate from, @NotNull LocalDate to) { + return withConnection(connection -> { + int botKeyId = botKeyData.getId(botId); + int serverKeyId = serverKeyData.getId(serverId); + return dao.ttsCountTable().sumMessageCountRange(connection, botKeyId, serverKeyId, from, to); + }, 0L); } @Override public long sumGlobalAllCharCount(long botId) { - try (Connection connection = dao.getConnection()) { + return withConnection(connection -> { int botKeyId = botKeyData.getId(botId); - return dao.ttsCountTable().sumAllCharCount(connection, botKeyId, null); - } catch (Exception e) { - fireErrorEvent(e); - throw new RuntimeException(e); - } catch (Throwable throwable) { - fireErrorEvent(throwable); - throw throwable; - } + return dao.ttsCountTable().sumAllCharCount(connection, botKeyId, DAO.TTSCountTable.GLOBAL_SERVER_KEY_ID); + }, 0L); } @Override public long sumGlobalAllMessageCount(long botId) { - try (Connection connection = dao.getConnection()) { + return withConnection(connection -> { int botKeyId = botKeyData.getId(botId); - return dao.ttsCountTable().sumAllMessageCount(connection, botKeyId, null); - } catch (Exception e) { - fireErrorEvent(e); - throw new RuntimeException(e); - } catch (Throwable throwable) { - fireErrorEvent(throwable); - throw throwable; - } + return dao.ttsCountTable().sumAllMessageCount(connection, botKeyId, DAO.TTSCountTable.GLOBAL_SERVER_KEY_ID); + }, 0L); } @Override public long sumServerAllCharCount(long botId, long serverId) { - try (Connection connection = dao.getConnection()) { + return withConnection(connection -> { int botKeyId = botKeyData.getId(botId); int serverKeyId = serverKeyData.getId(serverId); return dao.ttsCountTable().sumAllCharCount(connection, botKeyId, serverKeyId); - } catch (Exception e) { - fireErrorEvent(e); - throw new RuntimeException(e); - } catch (Throwable throwable) { - fireErrorEvent(throwable); - throw throwable; - } + }, 0L); } @Override public long sumServerAllMessageCount(long botId, long serverId) { - try (Connection connection = dao.getConnection()) { + return withConnection(connection -> { int botKeyId = botKeyData.getId(botId); int serverKeyId = serverKeyData.getId(serverId); return dao.ttsCountTable().sumAllMessageCount(connection, botKeyId, serverKeyId); + }, 0L); + } + + /** + * コネクションを取得して処理を実行する + * 例外発生時はエラーリスナーへ通知してフォールバック値を返す + * + * @param function 処理関数 + * @param fallback 例外時のフォールバック値 + * @param 戻り値型 + * @return 処理結果またはフォールバック値 + */ + private T withConnection(SQLFunction function, T fallback) { + try (Connection connection = dao.getConnection()) { + return function.apply(connection); } catch (Exception e) { fireErrorEvent(e); - throw new RuntimeException(e); + return fallback; } catch (Throwable throwable) { fireErrorEvent(throwable); - throw throwable; + return fallback; } } + /** + * SQL処理用の関数インタフェース + * + * @param 入力型 + * @param 戻り値型 + */ + @FunctionalInterface + private interface SQLFunction { + + /** + * 処理を適用する + * + * @param input 入力 + * @return 結果 + * @throws Exception 例外 + */ + R apply(T input) throws Exception; + } + /** * サーバーIDとユーザーIDで取得するキャッシュのキー * diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/repository/impl/TTSCountDataImpl.java b/core/src/main/java/dev/felnull/itts/core/savedata/repository/impl/TTSCountDataImpl.java index ece5532..dbea9a7 100644 --- a/core/src/main/java/dev/felnull/itts/core/savedata/repository/impl/TTSCountDataImpl.java +++ b/core/src/main/java/dev/felnull/itts/core/savedata/repository/impl/TTSCountDataImpl.java @@ -1,8 +1,8 @@ package dev.felnull.itts.core.savedata.repository.impl; +import dev.felnull.itts.core.savedata.dao.DAO; import dev.felnull.itts.core.savedata.dao.TTSCountRecord; import dev.felnull.itts.core.savedata.repository.TTSCountData; -import org.jetbrains.annotations.Nullable; import java.time.LocalDate; import java.util.Optional; @@ -18,49 +18,44 @@ class TTSCountDataImpl extends SaveDataBase implements TTSCountData { private final long botId; /** - * サーバーID nullの場合はBOT全体合計 + * サーバーID 0の場合はBOT全体合計 */ - @Nullable - private final Long serverId; + private final long serverId; /** * 集計日 */ private final LocalDate date; - TTSCountDataImpl(DataRepositoryImpl repository, long botId, @Nullable Long serverId, LocalDate date) { + TTSCountDataImpl(DataRepositoryImpl repository, long botId, long serverId, LocalDate date) { super(repository); this.botId = botId; this.serverId = serverId; this.date = date; } + private int resolveServerKeyId() { + if (serverId == 0L) { + return DAO.TTSCountTable.GLOBAL_SERVER_KEY_ID; + } + return repository.getServerKeyData().getId(serverId); + } + @Override public void addCount(long charDelta, long messageDelta) { sqlProc(connection -> { int botKeyId = repository.getBotKeyData().getId(botId); - Integer serverKeyId = serverId == null ? null : repository.getServerKeyData().getId(serverId); + int serverKeyId = resolveServerKeyId(); dao().ttsCountTable().incrementCount(connection, botKeyId, serverKeyId, date, charDelta, messageDelta); }); } @Override - public long getCharCount() { - return sqlProcReturnable(connection -> { - int botKeyId = repository.getBotKeyData().getId(botId); - Integer serverKeyId = serverId == null ? null : repository.getServerKeyData().getId(serverId); - Optional rec = dao().ttsCountTable().getCount(connection, botKeyId, serverKeyId, date); - return rec.map(TTSCountRecord::charCount).orElse(0L); - }); - } - - @Override - public long getMessageCount() { + public Optional getRecord() { return sqlProcReturnable(connection -> { int botKeyId = repository.getBotKeyData().getId(botId); - Integer serverKeyId = serverId == null ? null : repository.getServerKeyData().getId(serverId); - Optional rec = dao().ttsCountTable().getCount(connection, botKeyId, serverKeyId, date); - return rec.map(TTSCountRecord::messageCount).orElse(0L); + int serverKeyId = resolveServerKeyId(); + return dao().ttsCountTable().getCount(connection, botKeyId, serverKeyId, date); }); } } diff --git a/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java b/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java index d629a55..7ddbc2e 100644 --- a/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java +++ b/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java @@ -45,8 +45,8 @@ public void record(long botDiscordId, long guildDiscordId, int charCount) { try { metricsRegistry.getOrCreateCharCounter(botDiscordId, guildDiscordId).increment(charCount); metricsRegistry.getOrCreateMessageCounter(botDiscordId, guildDiscordId).increment(); - metricsRegistry.getOrCreateCharCounter(botDiscordId, null).increment(charCount); - metricsRegistry.getOrCreateMessageCounter(botDiscordId, null).increment(); + metricsRegistry.getOrCreateCharCounter(botDiscordId, MetricsRegistry.GLOBAL_SERVER_ID).increment(charCount); + metricsRegistry.getOrCreateMessageCounter(botDiscordId, MetricsRegistry.GLOBAL_SERVER_ID).increment(); } catch (Throwable t) { getITTSLogger().warn("Failed to update metrics counter", t); } From 67bb0828952812ab59c18cbb0ecff64aab89ab61 Mon Sep 17 00:00:00 2001 From: yuu1111 Date: Fri, 8 May 2026 06:13:08 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=E8=B2=AC=E5=8B=99=E5=88=86=E9=9B=A2?= =?UTF-8?q?=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MetricsRegistryをinterface化しPrometheus実装とNoOp実装を分離 メトリクス無効時もnon-nullになり呼び出し側のnullチェックを削除 - ITTSRuntimeUseにgetTTSCountRecorder/getMetricsRegistryのdefaultを追加 VoiceAudioSchedulerのITTSRuntime.getInstance()プルを既存パターンに統一 - ITTSRuntime#initMetricsを生成と起動に限定し、シャットダウンフック登録を registerShutdownHooksに分離 --- .../dev/felnull/itts/core/ITTSRuntime.java | 37 +++-- .../dev/felnull/itts/core/ITTSRuntimeUse.java | 21 +++ .../itts/core/audio/VoiceAudioScheduler.java | 3 +- .../itts/core/metrics/MetricsRegistry.java | 117 ++-------------- .../core/metrics/NoOpMetricsRegistry.java | 50 +++++++ .../core/metrics/PrometheusHttpExposer.java | 8 +- .../metrics/PrometheusMetricsRegistry.java | 127 ++++++++++++++++++ .../itts/core/tts/TTSCountRecorder.java | 19 ++- 8 files changed, 250 insertions(+), 132 deletions(-) create mode 100644 core/src/main/java/dev/felnull/itts/core/metrics/NoOpMetricsRegistry.java create mode 100644 core/src/main/java/dev/felnull/itts/core/metrics/PrometheusMetricsRegistry.java diff --git a/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java b/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java index ae2ce1e..3e866b5 100644 --- a/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java +++ b/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java @@ -9,6 +9,7 @@ import dev.felnull.itts.core.discord.Bot; import dev.felnull.itts.core.metrics.MetricsRegistry; import dev.felnull.itts.core.metrics.PrometheusHttpExposer; +import dev.felnull.itts.core.metrics.PrometheusMetricsRegistry; import dev.felnull.itts.core.savedata.SaveDataManager; import dev.felnull.itts.core.tts.TTSCountRecorder; import dev.felnull.itts.core.tts.TTSManager; @@ -134,7 +135,8 @@ public class ITTSRuntime { private long startupTime; /** - * Prometheusメトリクスのレジストリ + * メトリクスレジストリ + * 公開無効時はNoOp実装が入る */ private MetricsRegistry metricsRegistry; @@ -222,32 +224,48 @@ public void execute() { logger.info("Setup complete"); initMetrics(); + registerShutdownHooks(); bot.start(); } + /** + * メトリクス関連コンポーネントを生成し起動する + * 公開無効時はNoOp実装を採用しnullを返さない + */ private void initMetrics() { MetricsConfig metricsConfig = configManager.getConfig().getMetricsConfig(); - this.metricsRegistry = metricsConfig.isEnabled() ? new MetricsRegistry() : null; - this.ttsCountRecorder = new TTSCountRecorder(metricsRegistry); - if (metricsRegistry == null) { + if (!metricsConfig.isEnabled()) { + this.metricsRegistry = MetricsRegistry.noop(); + this.ttsCountRecorder = new TTSCountRecorder(metricsRegistry); logger.info("Prometheus metrics is disabled"); return; } + PrometheusMetricsRegistry prometheusRegistry = new PrometheusMetricsRegistry(); + this.metricsRegistry = prometheusRegistry; + this.ttsCountRecorder = new TTSCountRecorder(metricsRegistry); + try { - this.prometheusHttpExposer = new PrometheusHttpExposer(metricsRegistry); + this.prometheusHttpExposer = new PrometheusHttpExposer(prometheusRegistry); this.prometheusHttpExposer.start(metricsConfig.getBindAddress(), metricsConfig.getPort()); logger.info("Prometheus metrics endpoint started on {}:{}/metrics", metricsConfig.getBindAddress(), metricsConfig.getPort()); } catch (Exception e) { logger.warn("Failed to start Prometheus HTTP exposer", e); this.prometheusHttpExposer = null; } + } + /** + * シャットダウンフックを登録する + * メトリクスHTTPサーバなどライフサイクル管理対象を集約する + */ + private void registerShutdownHooks() { Runtime.getRuntime().addShutdownHook(new Thread(() -> { - if (this.prometheusHttpExposer != null) { - this.prometheusHttpExposer.stop(); + PrometheusHttpExposer exposer = this.prometheusHttpExposer; + if (exposer != null) { + exposer.stop(); } }, "prometheus-exposer-shutdown")); } @@ -331,9 +349,10 @@ public TTSCountRecorder getTTSCountRecorder() { } /** - * Prometheusメトリクスのレジストリを取得 + * メトリクスレジストリを取得 + * 公開無効時はNoOp実装が返るためnullにはならない * - * @return レジストリ nullの場合はメトリクス無効 + * @return メトリクスレジストリ */ public MetricsRegistry getMetricsRegistry() { return metricsRegistry; diff --git a/core/src/main/java/dev/felnull/itts/core/ITTSRuntimeUse.java b/core/src/main/java/dev/felnull/itts/core/ITTSRuntimeUse.java index 8c7c8f3..6664d7c 100644 --- a/core/src/main/java/dev/felnull/itts/core/ITTSRuntimeUse.java +++ b/core/src/main/java/dev/felnull/itts/core/ITTSRuntimeUse.java @@ -5,6 +5,8 @@ import dev.felnull.itts.core.config.ConfigManager; import dev.felnull.itts.core.dict.DictionaryManager; import dev.felnull.itts.core.discord.Bot; +import dev.felnull.itts.core.metrics.MetricsRegistry; +import dev.felnull.itts.core.tts.TTSCountRecorder; import dev.felnull.itts.core.tts.TTSManager; import dev.felnull.itts.core.voice.VoiceManager; import org.apache.logging.log4j.Logger; @@ -83,4 +85,23 @@ default Bot getBot() { default ITTSNetworkManager getNetworkManager() { return getITTSRuntime().getNetworkManager(); } + + /** + * 読み上げ文字数のレコーダーを取得 + * + * @return TTSカウントレコーダー + */ + default TTSCountRecorder getTTSCountRecorder() { + return getITTSRuntime().getTTSCountRecorder(); + } + + /** + * メトリクスレジストリを取得 + * 公開無効時もNoOp実装が返るためnullにはならない + * + * @return メトリクスレジストリ + */ + default MetricsRegistry getMetricsRegistry() { + return getITTSRuntime().getMetricsRegistry(); + } } diff --git a/core/src/main/java/dev/felnull/itts/core/audio/VoiceAudioScheduler.java b/core/src/main/java/dev/felnull/itts/core/audio/VoiceAudioScheduler.java index 8ca59da..053cbd3 100644 --- a/core/src/main/java/dev/felnull/itts/core/audio/VoiceAudioScheduler.java +++ b/core/src/main/java/dev/felnull/itts/core/audio/VoiceAudioScheduler.java @@ -4,7 +4,6 @@ import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason; -import dev.felnull.itts.core.ITTSRuntime; import dev.felnull.itts.core.ITTSRuntimeUse; import dev.felnull.itts.core.audio.loader.VoiceTrackLoader; import dev.felnull.itts.core.tts.TTSCountRecorder; @@ -98,7 +97,7 @@ public CompletableFuture load(SaidText saidText) { String finalText = TTSUtils.roundText(voice, guildId, sayText, false); - TTSCountRecorder recorder = ITTSRuntime.getInstance().getTTSCountRecorder(); + TTSCountRecorder recorder = getTTSCountRecorder(); if (recorder != null && finalText != null) { long botId = getBot().getBotId(); recorder.record(botId, guildId, finalText.length()); diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java b/core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java index 2b80b0b..b3420c3 100644 --- a/core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java +++ b/core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java @@ -1,95 +1,27 @@ package dev.felnull.itts.core.metrics; import io.micrometer.core.instrument.Counter; -import io.micrometer.core.instrument.Gauge; -import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics; -import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; -import io.micrometer.core.instrument.binder.system.ProcessorMetrics; -import io.micrometer.prometheusmetrics.PrometheusConfig; -import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; import org.jetbrains.annotations.NotNull; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - /** - * Prometheusメトリクスのレジストリ管理 + * メトリクスレジストリの抽象 + * 実装はPrometheus版とNoOp版を切り替える */ -public final class MetricsRegistry { +public interface MetricsRegistry { /** * BOT全体合計を表すサーバーIDの予約値 */ - public static final long GLOBAL_SERVER_ID = 0L; - - /** - * 文字数Counterのメトリクス名 - */ - private static final String CHAR_METRIC_NAME = "itts_spoken_chars_total"; + long GLOBAL_SERVER_ID = 0L; /** - * 文字数Counterの説明 - */ - private static final String CHAR_METRIC_DESCRIPTION = "Total spoken characters delivered to TTS API"; - - /** - * メッセージ数Counterのメトリクス名 - */ - private static final String MESSAGE_METRIC_NAME = "itts_spoken_messages_total"; - - /** - * メッセージ数Counterの説明 - */ - private static final String MESSAGE_METRIC_DESCRIPTION = "Total spoken messages delivered to TTS API"; - - /** - * Prometheus形式のレジストリ - */ - private final PrometheusMeterRegistry registry; - - /** - * 起動時刻のミリ秒 - */ - private final long bootAt; - - /** - * 文字数Counterのキャッシュ - */ - private final ConcurrentMap charCounters = new ConcurrentHashMap<>(); - - /** - * メッセージ数Counterのキャッシュ - */ - private final ConcurrentMap messageCounters = new ConcurrentHashMap<>(); - - /** - * コンストラクタ - */ - public MetricsRegistry() { - this.registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); - this.bootAt = System.currentTimeMillis(); - - Gauge.builder("itts_up", () -> 1.0d) - .description("Bot liveness indicator") - .register(registry); - - Gauge.builder("itts_uptime_seconds", () -> (System.currentTimeMillis() - bootAt) / 1000.0d) - .description("Bot uptime in seconds") - .register(registry); - - new JvmMemoryMetrics().bindTo(registry); - new JvmGcMetrics().bindTo(registry); - new ProcessorMetrics().bindTo(registry); - } - - /** - * Prometheusレジストリを取得 + * NoOp実装を返すファクトリ * - * @return レジストリ + * @return 何もしないMetricsRegistry */ @NotNull - public PrometheusMeterRegistry getRegistry() { - return registry; + static MetricsRegistry noop() { + return NoOpMetricsRegistry.INSTANCE; } /** @@ -100,9 +32,7 @@ public PrometheusMeterRegistry getRegistry() { * @return Counter */ @NotNull - public Counter getOrCreateCharCounter(long botId, long serverId) { - return getOrCreate(charCounters, CHAR_METRIC_NAME, CHAR_METRIC_DESCRIPTION, botId, serverId); - } + Counter getOrCreateCharCounter(long botId, long serverId); /** * メッセージ数Counterをラベル付きで取得 @@ -112,32 +42,5 @@ public Counter getOrCreateCharCounter(long botId, long serverId) { * @return Counter */ @NotNull - public Counter getOrCreateMessageCounter(long botId, long serverId) { - return getOrCreate(messageCounters, MESSAGE_METRIC_NAME, MESSAGE_METRIC_DESCRIPTION, botId, serverId); - } - - /** - * Counterをキャッシュから取得もしくは生成する - * - * @param cache Counterキャッシュ - * @param metricName メトリクス名 - * @param description メトリクスの説明 - * @param botId BOTのID - * @param serverId サーバーID GLOBAL_SERVER_IDの場合はBOT全体 - * @return Counter - */ - @NotNull - private Counter getOrCreate(@NotNull ConcurrentMap cache, - @NotNull String metricName, - @NotNull String description, - long botId, - long serverId) { - String serverTag = serverId == GLOBAL_SERVER_ID ? "global" : String.valueOf(serverId); - String key = botId + "|" + serverTag; - return cache.computeIfAbsent(key, k -> Counter.builder(metricName) - .description(description) - .tag("bot_id", String.valueOf(botId)) - .tag("server_id", serverTag) - .register(registry)); - } + Counter getOrCreateMessageCounter(long botId, long serverId); } diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/NoOpMetricsRegistry.java b/core/src/main/java/dev/felnull/itts/core/metrics/NoOpMetricsRegistry.java new file mode 100644 index 0000000..8f1b9f7 --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/metrics/NoOpMetricsRegistry.java @@ -0,0 +1,50 @@ +package dev.felnull.itts.core.metrics; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.noop.NoopCounter; +import org.jetbrains.annotations.NotNull; + +/** + * 何もしないMetricsRegistry実装 + * メトリクス公開無効時に利用しnullチェックを不要にする + */ +final class NoOpMetricsRegistry implements MetricsRegistry { + + /** + * 共有インスタンス + */ + static final NoOpMetricsRegistry INSTANCE = new NoOpMetricsRegistry(); + + /** + * 文字数Counterの共有NoOpインスタンス + */ + private static final Counter NOOP_CHAR_COUNTER = new NoopCounter( + new Meter.Id("itts_spoken_chars_total", Tags.empty(), null, null, Meter.Type.COUNTER)); + + /** + * メッセージ数Counterの共有NoOpインスタンス + */ + private static final Counter NOOP_MESSAGE_COUNTER = new NoopCounter( + new Meter.Id("itts_spoken_messages_total", Tags.empty(), null, null, Meter.Type.COUNTER)); + + /** + * コンストラクタ + * シングルトン用に外部からの生成を抑止する + */ + private NoOpMetricsRegistry() { + } + + @Override + @NotNull + public Counter getOrCreateCharCounter(long botId, long serverId) { + return NOOP_CHAR_COUNTER; + } + + @Override + @NotNull + public Counter getOrCreateMessageCounter(long botId, long serverId) { + return NOOP_MESSAGE_COUNTER; + } +} diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java b/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java index 46b2574..d302eab 100644 --- a/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java +++ b/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java @@ -28,9 +28,9 @@ public final class PrometheusHttpExposer { private static final int EXECUTOR_SHUTDOWN_TIMEOUT_SEC = 5; /** - * メトリクスレジストリ + * Prometheus形式のメトリクスレジストリ */ - private final MetricsRegistry metricsRegistry; + private final PrometheusMetricsRegistry metricsRegistry; /** * 内部HTTPサーバー @@ -45,9 +45,9 @@ public final class PrometheusHttpExposer { /** * コンストラクタ * - * @param metricsRegistry メトリクスレジストリ + * @param metricsRegistry Prometheus形式のメトリクスレジストリ */ - public PrometheusHttpExposer(@NotNull MetricsRegistry metricsRegistry) { + public PrometheusHttpExposer(@NotNull PrometheusMetricsRegistry metricsRegistry) { this.metricsRegistry = metricsRegistry; } diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusMetricsRegistry.java b/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusMetricsRegistry.java new file mode 100644 index 0000000..f27edce --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusMetricsRegistry.java @@ -0,0 +1,127 @@ +package dev.felnull.itts.core.metrics; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; +import io.micrometer.core.instrument.binder.system.ProcessorMetrics; +import io.micrometer.prometheusmetrics.PrometheusConfig; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Prometheus形式のメトリクスレジストリ実装 + * Counterのキャッシュとシステム系メトリクスのバインドを行う + */ +public final class PrometheusMetricsRegistry implements MetricsRegistry { + + /** + * 文字数Counterのメトリクス名 + */ + private static final String CHAR_METRIC_NAME = "itts_spoken_chars_total"; + + /** + * 文字数Counterの説明 + */ + private static final String CHAR_METRIC_DESCRIPTION = "Total spoken characters delivered to TTS API"; + + /** + * メッセージ数Counterのメトリクス名 + */ + private static final String MESSAGE_METRIC_NAME = "itts_spoken_messages_total"; + + /** + * メッセージ数Counterの説明 + */ + private static final String MESSAGE_METRIC_DESCRIPTION = "Total spoken messages delivered to TTS API"; + + /** + * Prometheus形式のレジストリ + */ + private final PrometheusMeterRegistry registry; + + /** + * 起動時刻のミリ秒 + */ + private final long bootAt; + + /** + * 文字数Counterのキャッシュ + */ + private final ConcurrentMap charCounters = new ConcurrentHashMap<>(); + + /** + * メッセージ数Counterのキャッシュ + */ + private final ConcurrentMap messageCounters = new ConcurrentHashMap<>(); + + /** + * コンストラクタ + */ + public PrometheusMetricsRegistry() { + this.registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); + this.bootAt = System.currentTimeMillis(); + + Gauge.builder("itts_up", () -> 1.0d) + .description("Bot liveness indicator") + .register(registry); + + Gauge.builder("itts_uptime_seconds", () -> (System.currentTimeMillis() - bootAt) / 1000.0d) + .description("Bot uptime in seconds") + .register(registry); + + new JvmMemoryMetrics().bindTo(registry); + new JvmGcMetrics().bindTo(registry); + new ProcessorMetrics().bindTo(registry); + } + + /** + * Prometheusレジストリを取得 + * + * @return レジストリ + */ + @NotNull + public PrometheusMeterRegistry getRegistry() { + return registry; + } + + @Override + @NotNull + public Counter getOrCreateCharCounter(long botId, long serverId) { + return getOrCreate(charCounters, CHAR_METRIC_NAME, CHAR_METRIC_DESCRIPTION, botId, serverId); + } + + @Override + @NotNull + public Counter getOrCreateMessageCounter(long botId, long serverId) { + return getOrCreate(messageCounters, MESSAGE_METRIC_NAME, MESSAGE_METRIC_DESCRIPTION, botId, serverId); + } + + /** + * Counterをキャッシュから取得もしくは生成する + * + * @param cache Counterキャッシュ + * @param metricName メトリクス名 + * @param description メトリクスの説明 + * @param botId BOTのID + * @param serverId サーバーID GLOBAL_SERVER_IDの場合はBOT全体 + * @return Counter + */ + @NotNull + private Counter getOrCreate(@NotNull ConcurrentMap cache, + @NotNull String metricName, + @NotNull String description, + long botId, + long serverId) { + String serverTag = serverId == GLOBAL_SERVER_ID ? "global" : String.valueOf(serverId); + String key = botId + "|" + serverTag; + return cache.computeIfAbsent(key, k -> Counter.builder(metricName) + .description(description) + .tag("bot_id", String.valueOf(botId)) + .tag("server_id", serverTag) + .register(registry)); + } +} diff --git a/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java b/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java index 7ddbc2e..ca97d74 100644 --- a/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java +++ b/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java @@ -16,7 +16,8 @@ public final class TTSCountRecorder implements ITTSRuntimeUse { /** - * メトリクスレジストリ nullの場合はメトリクス公開無効 + * メトリクスレジストリ + * 公開無効時はNoOp実装が渡されるためnullにはならない */ private final MetricsRegistry metricsRegistry; @@ -41,15 +42,13 @@ public void record(long botDiscordId, long guildDiscordId, int charCount) { return; } - if (metricsRegistry != null) { - try { - metricsRegistry.getOrCreateCharCounter(botDiscordId, guildDiscordId).increment(charCount); - metricsRegistry.getOrCreateMessageCounter(botDiscordId, guildDiscordId).increment(); - metricsRegistry.getOrCreateCharCounter(botDiscordId, MetricsRegistry.GLOBAL_SERVER_ID).increment(charCount); - metricsRegistry.getOrCreateMessageCounter(botDiscordId, MetricsRegistry.GLOBAL_SERVER_ID).increment(); - } catch (Throwable t) { - getITTSLogger().warn("Failed to update metrics counter", t); - } + try { + metricsRegistry.getOrCreateCharCounter(botDiscordId, guildDiscordId).increment(charCount); + metricsRegistry.getOrCreateMessageCounter(botDiscordId, guildDiscordId).increment(); + metricsRegistry.getOrCreateCharCounter(botDiscordId, MetricsRegistry.GLOBAL_SERVER_ID).increment(charCount); + metricsRegistry.getOrCreateMessageCounter(botDiscordId, MetricsRegistry.GLOBAL_SERVER_ID).increment(); + } catch (Throwable t) { + getITTSLogger().warn("Failed to update metrics counter", t); } CompletableFuture.runAsync(() -> writeToDatabase(botDiscordId, guildDiscordId, charCount), getAsyncExecutor()) From b994008e86c883a1e81896c5139daa7019f52cc0 Mon Sep 17 00:00:00 2001 From: yuu1111 Date: Fri, 8 May 2026 07:24:38 +0900 Subject: [PATCH 4/5] =?UTF-8?q?Prometheus=E3=83=A1=E3=83=88=E3=83=AA?= =?UTF-8?q?=E3=82=AF=E3=82=B9=E5=85=AC=E9=96=8B=E3=82=92=E5=88=86=E9=9B=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 集計機能のマージ後に別PRで対応する方針のため、 Prometheus関連の実装を本PRから取り除く - core/metrics配下を削除 - MetricsConfigを削除 - ITTSRuntime/Use・Config・selfhost ConfigImplから参照を除去 - Micrometer依存を削除 - TTSCountRecorderはDB書き込みのみに簡略化 --- core/build.gradle.kts | 3 - .../dev/felnull/itts/core/ITTSRuntime.java | 79 +---------- .../dev/felnull/itts/core/ITTSRuntimeUse.java | 11 -- .../dev/felnull/itts/core/config/Config.java | 9 -- .../itts/core/config/MetricsConfig.java | 66 --------- .../itts/core/metrics/MetricsRegistry.java | 46 ------- .../core/metrics/NoOpMetricsRegistry.java | 50 ------- .../core/metrics/PrometheusHttpExposer.java | 104 -------------- .../metrics/PrometheusMetricsRegistry.java | 127 ------------------ .../itts/core/metrics/package-info.java | 4 - .../itts/core/tts/TTSCountRecorder.java | 25 ---- .../dev/felnull/itts/config/ConfigImpl.java | 69 +--------- 12 files changed, 9 insertions(+), 584 deletions(-) delete mode 100644 core/src/main/java/dev/felnull/itts/core/config/MetricsConfig.java delete mode 100644 core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java delete mode 100644 core/src/main/java/dev/felnull/itts/core/metrics/NoOpMetricsRegistry.java delete mode 100644 core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java delete mode 100644 core/src/main/java/dev/felnull/itts/core/metrics/PrometheusMetricsRegistry.java delete mode 100644 core/src/main/java/dev/felnull/itts/core/metrics/package-info.java diff --git a/core/build.gradle.kts b/core/build.gradle.kts index d31de45..32f6aa4 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -37,9 +37,6 @@ dependencies { api("org.xerial:sqlite-jdbc:3.51.1.0") api("it.unimi.dsi:fastutil:8.5.18") - api("io.micrometer:micrometer-core:1.13.6") - api("io.micrometer:micrometer-registry-prometheus:1.13.6") - api("org.jetbrains:annotations:26.0.2-1") // api("org.apache.logging.log4j:log4j-core:3.0.0-beta3") diff --git a/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java b/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java index 3e866b5..293d1e0 100644 --- a/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java +++ b/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java @@ -4,12 +4,8 @@ import dev.felnull.itts.core.audio.VoiceAudioManager; import dev.felnull.itts.core.cache.CacheManager; import dev.felnull.itts.core.config.ConfigManager; -import dev.felnull.itts.core.config.MetricsConfig; import dev.felnull.itts.core.dict.DictionaryManager; import dev.felnull.itts.core.discord.Bot; -import dev.felnull.itts.core.metrics.MetricsRegistry; -import dev.felnull.itts.core.metrics.PrometheusHttpExposer; -import dev.felnull.itts.core.metrics.PrometheusMetricsRegistry; import dev.felnull.itts.core.savedata.SaveDataManager; import dev.felnull.itts.core.tts.TTSCountRecorder; import dev.felnull.itts.core.tts.TTSManager; @@ -124,6 +120,11 @@ public class ITTSRuntime { */ private final Bot bot; + /** + * 読み上げ文字数のレコーダー + */ + private final TTSCountRecorder ttsCountRecorder = new TTSCountRecorder(); + /** * 全てのマネージャー */ @@ -134,22 +135,6 @@ public class ITTSRuntime { */ private long startupTime; - /** - * メトリクスレジストリ - * 公開無効時はNoOp実装が入る - */ - private MetricsRegistry metricsRegistry; - - /** - * Prometheusメトリクス公開HTTPサーバー - */ - private PrometheusHttpExposer prometheusHttpExposer; - - /** - * 読み上げ文字数のレコーダー - */ - private TTSCountRecorder ttsCountRecorder; - private ITTSRuntime(ITTSRuntimeContext runtimeContext) { if (instance != null) { throw new IllegalStateException("ITTSRuntime must be a singleton instance"); @@ -223,53 +208,9 @@ public void execute() { logger.info("Setup complete"); - initMetrics(); - registerShutdownHooks(); - bot.start(); } - /** - * メトリクス関連コンポーネントを生成し起動する - * 公開無効時はNoOp実装を採用しnullを返さない - */ - private void initMetrics() { - MetricsConfig metricsConfig = configManager.getConfig().getMetricsConfig(); - - if (!metricsConfig.isEnabled()) { - this.metricsRegistry = MetricsRegistry.noop(); - this.ttsCountRecorder = new TTSCountRecorder(metricsRegistry); - logger.info("Prometheus metrics is disabled"); - return; - } - - PrometheusMetricsRegistry prometheusRegistry = new PrometheusMetricsRegistry(); - this.metricsRegistry = prometheusRegistry; - this.ttsCountRecorder = new TTSCountRecorder(metricsRegistry); - - try { - this.prometheusHttpExposer = new PrometheusHttpExposer(prometheusRegistry); - this.prometheusHttpExposer.start(metricsConfig.getBindAddress(), metricsConfig.getPort()); - logger.info("Prometheus metrics endpoint started on {}:{}/metrics", metricsConfig.getBindAddress(), metricsConfig.getPort()); - } catch (Exception e) { - logger.warn("Failed to start Prometheus HTTP exposer", e); - this.prometheusHttpExposer = null; - } - } - - /** - * シャットダウンフックを登録する - * メトリクスHTTPサーバなどライフサイクル管理対象を集約する - */ - private void registerShutdownHooks() { - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - PrometheusHttpExposer exposer = this.prometheusHttpExposer; - if (exposer != null) { - exposer.stop(); - } - }, "prometheus-exposer-shutdown")); - } - public long getStartupTime() { return startupTime; } @@ -347,14 +288,4 @@ public Bot getBot() { public TTSCountRecorder getTTSCountRecorder() { return ttsCountRecorder; } - - /** - * メトリクスレジストリを取得 - * 公開無効時はNoOp実装が返るためnullにはならない - * - * @return メトリクスレジストリ - */ - public MetricsRegistry getMetricsRegistry() { - return metricsRegistry; - } } diff --git a/core/src/main/java/dev/felnull/itts/core/ITTSRuntimeUse.java b/core/src/main/java/dev/felnull/itts/core/ITTSRuntimeUse.java index 6664d7c..a399808 100644 --- a/core/src/main/java/dev/felnull/itts/core/ITTSRuntimeUse.java +++ b/core/src/main/java/dev/felnull/itts/core/ITTSRuntimeUse.java @@ -5,7 +5,6 @@ import dev.felnull.itts.core.config.ConfigManager; import dev.felnull.itts.core.dict.DictionaryManager; import dev.felnull.itts.core.discord.Bot; -import dev.felnull.itts.core.metrics.MetricsRegistry; import dev.felnull.itts.core.tts.TTSCountRecorder; import dev.felnull.itts.core.tts.TTSManager; import dev.felnull.itts.core.voice.VoiceManager; @@ -94,14 +93,4 @@ default ITTSNetworkManager getNetworkManager() { default TTSCountRecorder getTTSCountRecorder() { return getITTSRuntime().getTTSCountRecorder(); } - - /** - * メトリクスレジストリを取得 - * 公開無効時もNoOp実装が返るためnullにはならない - * - * @return メトリクスレジストリ - */ - default MetricsRegistry getMetricsRegistry() { - return getITTSRuntime().getMetricsRegistry(); - } } diff --git a/core/src/main/java/dev/felnull/itts/core/config/Config.java b/core/src/main/java/dev/felnull/itts/core/config/Config.java index 6a733d0..5d68ede 100644 --- a/core/src/main/java/dev/felnull/itts/core/config/Config.java +++ b/core/src/main/java/dev/felnull/itts/core/config/Config.java @@ -82,13 +82,4 @@ public interface Config { * @return DB関係のコンフィグ */ DataBaseConfig getDataBaseConfig(); - - /** - * Prometheusメトリクスのコンフィグを取得 - * - * @return メトリクスコンフィグ - */ - default MetricsConfig getMetricsConfig() { - return MetricsConfig.DEFAULT; - } } diff --git a/core/src/main/java/dev/felnull/itts/core/config/MetricsConfig.java b/core/src/main/java/dev/felnull/itts/core/config/MetricsConfig.java deleted file mode 100644 index 7516723..0000000 --- a/core/src/main/java/dev/felnull/itts/core/config/MetricsConfig.java +++ /dev/null @@ -1,66 +0,0 @@ -package dev.felnull.itts.core.config; - -import org.jetbrains.annotations.NotNull; - -/** - * Prometheusメトリクス公開のコンフィグ - */ -public interface MetricsConfig { - - /** - * デフォルトの有効状態 - */ - boolean DEFAULT_ENABLED = false; - - /** - * デフォルトのバインドアドレス - */ - String DEFAULT_BIND_ADDRESS = "127.0.0.1"; - - /** - * デフォルトのポート番号 - */ - int DEFAULT_PORT = 9095; - - /** - * デフォルトのコンフィグインスタンス - */ - MetricsConfig DEFAULT = new MetricsConfig() { - @Override - public boolean isEnabled() { - return DEFAULT_ENABLED; - } - - @Override - public @NotNull String getBindAddress() { - return DEFAULT_BIND_ADDRESS; - } - - @Override - public int getPort() { - return DEFAULT_PORT; - } - }; - - /** - * 有効かどうかを取得 - * - * @return 有効かどうか - */ - boolean isEnabled(); - - /** - * バインドアドレスを取得 - * - * @return バインドアドレス - */ - @NotNull - String getBindAddress(); - - /** - * ポート番号を取得 - * - * @return ポート番号 - */ - int getPort(); -} diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java b/core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java deleted file mode 100644 index b3420c3..0000000 --- a/core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java +++ /dev/null @@ -1,46 +0,0 @@ -package dev.felnull.itts.core.metrics; - -import io.micrometer.core.instrument.Counter; -import org.jetbrains.annotations.NotNull; - -/** - * メトリクスレジストリの抽象 - * 実装はPrometheus版とNoOp版を切り替える - */ -public interface MetricsRegistry { - - /** - * BOT全体合計を表すサーバーIDの予約値 - */ - long GLOBAL_SERVER_ID = 0L; - - /** - * NoOp実装を返すファクトリ - * - * @return 何もしないMetricsRegistry - */ - @NotNull - static MetricsRegistry noop() { - return NoOpMetricsRegistry.INSTANCE; - } - - /** - * 文字数Counterをラベル付きで取得 - * - * @param botId BOTのID - * @param serverId サーバーID GLOBAL_SERVER_IDの場合はBOT全体 - * @return Counter - */ - @NotNull - Counter getOrCreateCharCounter(long botId, long serverId); - - /** - * メッセージ数Counterをラベル付きで取得 - * - * @param botId BOTのID - * @param serverId サーバーID GLOBAL_SERVER_IDの場合はBOT全体 - * @return Counter - */ - @NotNull - Counter getOrCreateMessageCounter(long botId, long serverId); -} diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/NoOpMetricsRegistry.java b/core/src/main/java/dev/felnull/itts/core/metrics/NoOpMetricsRegistry.java deleted file mode 100644 index 8f1b9f7..0000000 --- a/core/src/main/java/dev/felnull/itts/core/metrics/NoOpMetricsRegistry.java +++ /dev/null @@ -1,50 +0,0 @@ -package dev.felnull.itts.core.metrics; - -import io.micrometer.core.instrument.Counter; -import io.micrometer.core.instrument.Meter; -import io.micrometer.core.instrument.Tags; -import io.micrometer.core.instrument.noop.NoopCounter; -import org.jetbrains.annotations.NotNull; - -/** - * 何もしないMetricsRegistry実装 - * メトリクス公開無効時に利用しnullチェックを不要にする - */ -final class NoOpMetricsRegistry implements MetricsRegistry { - - /** - * 共有インスタンス - */ - static final NoOpMetricsRegistry INSTANCE = new NoOpMetricsRegistry(); - - /** - * 文字数Counterの共有NoOpインスタンス - */ - private static final Counter NOOP_CHAR_COUNTER = new NoopCounter( - new Meter.Id("itts_spoken_chars_total", Tags.empty(), null, null, Meter.Type.COUNTER)); - - /** - * メッセージ数Counterの共有NoOpインスタンス - */ - private static final Counter NOOP_MESSAGE_COUNTER = new NoopCounter( - new Meter.Id("itts_spoken_messages_total", Tags.empty(), null, null, Meter.Type.COUNTER)); - - /** - * コンストラクタ - * シングルトン用に外部からの生成を抑止する - */ - private NoOpMetricsRegistry() { - } - - @Override - @NotNull - public Counter getOrCreateCharCounter(long botId, long serverId) { - return NOOP_CHAR_COUNTER; - } - - @Override - @NotNull - public Counter getOrCreateMessageCounter(long botId, long serverId) { - return NOOP_MESSAGE_COUNTER; - } -} diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java b/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java deleted file mode 100644 index d302eab..0000000 --- a/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java +++ /dev/null @@ -1,104 +0,0 @@ -package dev.felnull.itts.core.metrics; - -import com.sun.net.httpserver.HttpServer; -import org.apache.commons.lang3.concurrent.BasicThreadFactory; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -/** - * PrometheusメトリクスをHTTPで公開するエクスポーザ - */ -public final class PrometheusHttpExposer { - - /** - * リクエスト処理スレッドプールのスレッド数 - */ - private static final int EXECUTOR_THREAD_COUNT = 2; - - /** - * Executor停止時の待機秒数 - */ - private static final int EXECUTOR_SHUTDOWN_TIMEOUT_SEC = 5; - - /** - * Prometheus形式のメトリクスレジストリ - */ - private final PrometheusMetricsRegistry metricsRegistry; - - /** - * 内部HTTPサーバー - */ - private HttpServer httpServer; - - /** - * リクエスト処理用スレッドプール - */ - private ExecutorService executor; - - /** - * コンストラクタ - * - * @param metricsRegistry Prometheus形式のメトリクスレジストリ - */ - public PrometheusHttpExposer(@NotNull PrometheusMetricsRegistry metricsRegistry) { - this.metricsRegistry = metricsRegistry; - } - - /** - * 起動する - * - * @param host バインドアドレス - * @param port ポート番号 - * @throws IOException 起動失敗時 - */ - public void start(@NotNull String host, int port) throws IOException { - HttpServer server = HttpServer.create(new InetSocketAddress(host, port), 0); - server.createContext("/metrics", exchange -> { - String body = metricsRegistry.getRegistry().scrape(); - byte[] bytes = body.getBytes(StandardCharsets.UTF_8); - exchange.getResponseHeaders().add("Content-Type", "text/plain; version=0.0.4; charset=utf-8"); - exchange.sendResponseHeaders(200, bytes.length); - try (OutputStream out = exchange.getResponseBody()) { - out.write(bytes); - } - }); - ExecutorService pool = Executors.newFixedThreadPool(EXECUTOR_THREAD_COUNT, - new BasicThreadFactory.Builder() - .namingPattern("prometheus-exposer-%d") - .daemon(true) - .build()); - server.setExecutor(pool); - server.start(); - this.httpServer = server; - this.executor = pool; - } - - /** - * 停止する - */ - public void stop() { - if (httpServer != null) { - httpServer.stop(0); - httpServer = null; - } - if (executor != null) { - executor.shutdown(); - try { - if (!executor.awaitTermination(EXECUTOR_SHUTDOWN_TIMEOUT_SEC, TimeUnit.SECONDS)) { - executor.shutdownNow(); - } - } catch (InterruptedException e) { - executor.shutdownNow(); - Thread.currentThread().interrupt(); - } - executor = null; - } - } -} diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusMetricsRegistry.java b/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusMetricsRegistry.java deleted file mode 100644 index f27edce..0000000 --- a/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusMetricsRegistry.java +++ /dev/null @@ -1,127 +0,0 @@ -package dev.felnull.itts.core.metrics; - -import io.micrometer.core.instrument.Counter; -import io.micrometer.core.instrument.Gauge; -import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics; -import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; -import io.micrometer.core.instrument.binder.system.ProcessorMetrics; -import io.micrometer.prometheusmetrics.PrometheusConfig; -import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; -import org.jetbrains.annotations.NotNull; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -/** - * Prometheus形式のメトリクスレジストリ実装 - * Counterのキャッシュとシステム系メトリクスのバインドを行う - */ -public final class PrometheusMetricsRegistry implements MetricsRegistry { - - /** - * 文字数Counterのメトリクス名 - */ - private static final String CHAR_METRIC_NAME = "itts_spoken_chars_total"; - - /** - * 文字数Counterの説明 - */ - private static final String CHAR_METRIC_DESCRIPTION = "Total spoken characters delivered to TTS API"; - - /** - * メッセージ数Counterのメトリクス名 - */ - private static final String MESSAGE_METRIC_NAME = "itts_spoken_messages_total"; - - /** - * メッセージ数Counterの説明 - */ - private static final String MESSAGE_METRIC_DESCRIPTION = "Total spoken messages delivered to TTS API"; - - /** - * Prometheus形式のレジストリ - */ - private final PrometheusMeterRegistry registry; - - /** - * 起動時刻のミリ秒 - */ - private final long bootAt; - - /** - * 文字数Counterのキャッシュ - */ - private final ConcurrentMap charCounters = new ConcurrentHashMap<>(); - - /** - * メッセージ数Counterのキャッシュ - */ - private final ConcurrentMap messageCounters = new ConcurrentHashMap<>(); - - /** - * コンストラクタ - */ - public PrometheusMetricsRegistry() { - this.registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); - this.bootAt = System.currentTimeMillis(); - - Gauge.builder("itts_up", () -> 1.0d) - .description("Bot liveness indicator") - .register(registry); - - Gauge.builder("itts_uptime_seconds", () -> (System.currentTimeMillis() - bootAt) / 1000.0d) - .description("Bot uptime in seconds") - .register(registry); - - new JvmMemoryMetrics().bindTo(registry); - new JvmGcMetrics().bindTo(registry); - new ProcessorMetrics().bindTo(registry); - } - - /** - * Prometheusレジストリを取得 - * - * @return レジストリ - */ - @NotNull - public PrometheusMeterRegistry getRegistry() { - return registry; - } - - @Override - @NotNull - public Counter getOrCreateCharCounter(long botId, long serverId) { - return getOrCreate(charCounters, CHAR_METRIC_NAME, CHAR_METRIC_DESCRIPTION, botId, serverId); - } - - @Override - @NotNull - public Counter getOrCreateMessageCounter(long botId, long serverId) { - return getOrCreate(messageCounters, MESSAGE_METRIC_NAME, MESSAGE_METRIC_DESCRIPTION, botId, serverId); - } - - /** - * Counterをキャッシュから取得もしくは生成する - * - * @param cache Counterキャッシュ - * @param metricName メトリクス名 - * @param description メトリクスの説明 - * @param botId BOTのID - * @param serverId サーバーID GLOBAL_SERVER_IDの場合はBOT全体 - * @return Counter - */ - @NotNull - private Counter getOrCreate(@NotNull ConcurrentMap cache, - @NotNull String metricName, - @NotNull String description, - long botId, - long serverId) { - String serverTag = serverId == GLOBAL_SERVER_ID ? "global" : String.valueOf(serverId); - String key = botId + "|" + serverTag; - return cache.computeIfAbsent(key, k -> Counter.builder(metricName) - .description(description) - .tag("bot_id", String.valueOf(botId)) - .tag("server_id", serverTag) - .register(registry)); - } -} diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/package-info.java b/core/src/main/java/dev/felnull/itts/core/metrics/package-info.java deleted file mode 100644 index 8ab66c6..0000000 --- a/core/src/main/java/dev/felnull/itts/core/metrics/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Prometheusメトリクスの収集と公開 - */ -package dev.felnull.itts.core.metrics; diff --git a/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java b/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java index ca97d74..64ac100 100644 --- a/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java +++ b/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java @@ -1,7 +1,6 @@ package dev.felnull.itts.core.tts; import dev.felnull.itts.core.ITTSRuntimeUse; -import dev.felnull.itts.core.metrics.MetricsRegistry; import dev.felnull.itts.core.savedata.SaveDataManager; import dev.felnull.itts.core.savedata.repository.DataRepository; import dev.felnull.itts.core.savedata.repository.TTSCountData; @@ -15,21 +14,6 @@ */ public final class TTSCountRecorder implements ITTSRuntimeUse { - /** - * メトリクスレジストリ - * 公開無効時はNoOp実装が渡されるためnullにはならない - */ - private final MetricsRegistry metricsRegistry; - - /** - * コンストラクタ - * - * @param metricsRegistry メトリクスレジストリ - */ - public TTSCountRecorder(MetricsRegistry metricsRegistry) { - this.metricsRegistry = metricsRegistry; - } - /** * 読み上げ文字数を記録する * @@ -42,15 +26,6 @@ public void record(long botDiscordId, long guildDiscordId, int charCount) { return; } - try { - metricsRegistry.getOrCreateCharCounter(botDiscordId, guildDiscordId).increment(charCount); - metricsRegistry.getOrCreateMessageCounter(botDiscordId, guildDiscordId).increment(); - metricsRegistry.getOrCreateCharCounter(botDiscordId, MetricsRegistry.GLOBAL_SERVER_ID).increment(charCount); - metricsRegistry.getOrCreateMessageCounter(botDiscordId, MetricsRegistry.GLOBAL_SERVER_ID).increment(); - } catch (Throwable t) { - getITTSLogger().warn("Failed to update metrics counter", t); - } - CompletableFuture.runAsync(() -> writeToDatabase(botDiscordId, guildDiscordId, charCount), getAsyncExecutor()) .exceptionally(throwable -> { getITTSLogger().warn("Failed to record TTS count", throwable); diff --git a/selfhost/src/main/java/dev/felnull/itts/config/ConfigImpl.java b/selfhost/src/main/java/dev/felnull/itts/config/ConfigImpl.java index 0967ec6..704dd5f 100644 --- a/selfhost/src/main/java/dev/felnull/itts/config/ConfigImpl.java +++ b/selfhost/src/main/java/dev/felnull/itts/config/ConfigImpl.java @@ -5,7 +5,6 @@ import dev.felnull.itts.config.old.ConfigV0; import dev.felnull.itts.core.config.Config; import dev.felnull.itts.core.config.DataBaseConfig; -import dev.felnull.itts.core.config.MetricsConfig; import dev.felnull.itts.core.config.voicetype.VoiceTextConfig; import dev.felnull.itts.core.config.voicetype.VoicevoxConfig; import dev.felnull.itts.core.util.NameSerializableEnum; @@ -27,7 +26,6 @@ * @param coeirolnkConfig COEIROLNK コンフィグ * @param sharevoxConfig SHAREVOX コンフィグ * @param dataBaseConfig データベースコンフィグ - * @param metricsConfig Prometheusメトリクスコンフィグ */ public record ConfigImpl( String botToken, @@ -37,8 +35,7 @@ public record ConfigImpl( VoicevoxConfig voicevoxConfig, VoicevoxConfig coeirolnkConfig, VoicevoxConfig sharevoxConfig, - DataBaseConfig dataBaseConfig, - MetricsConfig metricsConfig + DataBaseConfig dataBaseConfig ) implements Config { /** @@ -55,7 +52,6 @@ public ConfigImpl load(JsonObject json5) { VoicevoxConfig coeirolnkConfig = VoicevoxConfigImpl.fromJson(Optional.ofNullable(json5.getObject("coeirolnk")).orElseGet(JsonObject::new)); VoicevoxConfig sharevoxConfig = VoicevoxConfigImpl.fromJson(Optional.ofNullable(json5.getObject("sharevox")).orElseGet(JsonObject::new)); DataBaseConfig dataBaseConfig = DataBaseConfigImpl.fromJson(Optional.ofNullable(json5.getObject("data_base")).orElseGet(JsonObject::new)); - MetricsConfig metricsConfig = MetricsConfigImpl.fromJson(Optional.ofNullable(json5.getObject("metrics")).orElseGet(JsonObject::new)); return new ConfigImpl( botToken, @@ -65,8 +61,7 @@ public ConfigImpl load(JsonObject json5) { voicevoxConfig, coeirolnkConfig, sharevoxConfig, - dataBaseConfig, - metricsConfig + dataBaseConfig ); } @@ -82,8 +77,7 @@ public ConfigImpl migrate(Object oldConfig) { new VoicevoxConfigImpl(configV0.voicevoxConfig().enable(), configV0.voicevoxConfig().apiUrls(), configV0.voicevoxConfig().checkTime()), new VoicevoxConfigImpl(configV0.coeirolnkConfig().enable(), configV0.coeirolnkConfig().apiUrls(), configV0.coeirolnkConfig().checkTime()), new VoicevoxConfigImpl(configV0.sharevoxConfig().enable(), configV0.sharevoxConfig().apiUrls(), configV0.sharevoxConfig().checkTime()), - new DataBaseConfigImpl(), - new MetricsConfigImpl() + new DataBaseConfigImpl() ); } }; @@ -102,8 +96,7 @@ public static ConfigImpl createInitialConfig() { new VoicevoxConfigImpl(), new VoicevoxConfigImpl(), new VoicevoxConfigImpl(), - new DataBaseConfigImpl(), - new MetricsConfigImpl() + new DataBaseConfigImpl() ); } @@ -119,7 +112,6 @@ public void writeToJson(JsonObject json5) { json5.put("coeirolnk", ((VoicevoxConfigImpl) this.coeirolnkConfig).toJson(), "COEIROLNKのコンフィグ"); json5.put("sharevox", ((VoicevoxConfigImpl) this.sharevoxConfig).toJson(), "SHAREVOXのコンフィグ"); json5.put("data_base", ((DataBaseConfigImpl) this.dataBaseConfig).toJson(), "データベースのコンフィグ"); - json5.put("metrics", ((MetricsConfigImpl) this.metricsConfig).toJson(), "Prometheusメトリクスのコンフィグ"); } @Override @@ -162,11 +154,6 @@ public DataBaseConfig getDataBaseConfig() { return dataBaseConfig; } - @Override - public MetricsConfig getMetricsConfig() { - return metricsConfig; - } - /** * VOICETEXTコンフィグの実装 * @@ -326,52 +313,4 @@ public JsonObject toJson() { return password; } } - - /** - * Prometheusメトリクスコンフィグの実装 - * - * @param enabled 有効かどうか - * @param bindAddress バインドアドレス - * @param port ポート番号 - */ - private record MetricsConfigImpl( - boolean enabled, - String bindAddress, - @Range(from = 0, to = 65535) int port - ) implements MetricsConfig { - - private MetricsConfigImpl() { - this(DEFAULT_ENABLED, DEFAULT_BIND_ADDRESS, DEFAULT_PORT); - } - - public static MetricsConfigImpl fromJson(JsonObject jo) { - boolean enabled = jo.getBoolean("enable", DEFAULT_ENABLED); - String bindAddress = Json5Utils.getStringOrElse(jo, "bind_address", DEFAULT_BIND_ADDRESS); - int port = jo.getInt("port", DEFAULT_PORT); - return new MetricsConfigImpl(enabled, bindAddress, port); - } - - public JsonObject toJson() { - JsonObject jo = new JsonObject(); - jo.put("enable", JsonPrimitive.of(enabled), "Prometheusメトリクス公開を有効にするかどうか"); - jo.put("bind_address", JsonPrimitive.of(bindAddress), "メトリクスHTTPサーバーのバインドアドレス"); - jo.put("port", new JsonPrimitive((long) port), "メトリクスHTTPサーバーのポート番号"); - return jo; - } - - @Override - public boolean isEnabled() { - return enabled; - } - - @Override - public @NotNull String getBindAddress() { - return bindAddress; - } - - @Override - public @Range(from = 0, to = 65535) int getPort() { - return port; - } - } } From de7bd205096d95906958e0b9421f561129692d42 Mon Sep 17 00:00:00 2001 From: yuu1111 Date: Fri, 8 May 2026 07:25:43 +0900 Subject: [PATCH 5/5] =?UTF-8?q?Revert=20"Prometheus=E3=83=A1=E3=83=88?= =?UTF-8?q?=E3=83=AA=E3=82=AF=E3=82=B9=E5=85=AC=E9=96=8B=E3=82=92=E5=88=86?= =?UTF-8?q?=E9=9B=A2"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit b994008e86c883a1e81896c5139daa7019f52cc0. --- core/build.gradle.kts | 3 + .../dev/felnull/itts/core/ITTSRuntime.java | 79 ++++++++++- .../dev/felnull/itts/core/ITTSRuntimeUse.java | 11 ++ .../dev/felnull/itts/core/config/Config.java | 9 ++ .../itts/core/config/MetricsConfig.java | 66 +++++++++ .../itts/core/metrics/MetricsRegistry.java | 46 +++++++ .../core/metrics/NoOpMetricsRegistry.java | 50 +++++++ .../core/metrics/PrometheusHttpExposer.java | 104 ++++++++++++++ .../metrics/PrometheusMetricsRegistry.java | 127 ++++++++++++++++++ .../itts/core/metrics/package-info.java | 4 + .../itts/core/tts/TTSCountRecorder.java | 25 ++++ .../dev/felnull/itts/config/ConfigImpl.java | 69 +++++++++- 12 files changed, 584 insertions(+), 9 deletions(-) create mode 100644 core/src/main/java/dev/felnull/itts/core/config/MetricsConfig.java create mode 100644 core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java create mode 100644 core/src/main/java/dev/felnull/itts/core/metrics/NoOpMetricsRegistry.java create mode 100644 core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java create mode 100644 core/src/main/java/dev/felnull/itts/core/metrics/PrometheusMetricsRegistry.java create mode 100644 core/src/main/java/dev/felnull/itts/core/metrics/package-info.java diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 32f6aa4..d31de45 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -37,6 +37,9 @@ dependencies { api("org.xerial:sqlite-jdbc:3.51.1.0") api("it.unimi.dsi:fastutil:8.5.18") + api("io.micrometer:micrometer-core:1.13.6") + api("io.micrometer:micrometer-registry-prometheus:1.13.6") + api("org.jetbrains:annotations:26.0.2-1") // api("org.apache.logging.log4j:log4j-core:3.0.0-beta3") diff --git a/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java b/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java index 293d1e0..3e866b5 100644 --- a/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java +++ b/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java @@ -4,8 +4,12 @@ import dev.felnull.itts.core.audio.VoiceAudioManager; import dev.felnull.itts.core.cache.CacheManager; import dev.felnull.itts.core.config.ConfigManager; +import dev.felnull.itts.core.config.MetricsConfig; import dev.felnull.itts.core.dict.DictionaryManager; import dev.felnull.itts.core.discord.Bot; +import dev.felnull.itts.core.metrics.MetricsRegistry; +import dev.felnull.itts.core.metrics.PrometheusHttpExposer; +import dev.felnull.itts.core.metrics.PrometheusMetricsRegistry; import dev.felnull.itts.core.savedata.SaveDataManager; import dev.felnull.itts.core.tts.TTSCountRecorder; import dev.felnull.itts.core.tts.TTSManager; @@ -120,11 +124,6 @@ public class ITTSRuntime { */ private final Bot bot; - /** - * 読み上げ文字数のレコーダー - */ - private final TTSCountRecorder ttsCountRecorder = new TTSCountRecorder(); - /** * 全てのマネージャー */ @@ -135,6 +134,22 @@ public class ITTSRuntime { */ private long startupTime; + /** + * メトリクスレジストリ + * 公開無効時はNoOp実装が入る + */ + private MetricsRegistry metricsRegistry; + + /** + * Prometheusメトリクス公開HTTPサーバー + */ + private PrometheusHttpExposer prometheusHttpExposer; + + /** + * 読み上げ文字数のレコーダー + */ + private TTSCountRecorder ttsCountRecorder; + private ITTSRuntime(ITTSRuntimeContext runtimeContext) { if (instance != null) { throw new IllegalStateException("ITTSRuntime must be a singleton instance"); @@ -208,9 +223,53 @@ public void execute() { logger.info("Setup complete"); + initMetrics(); + registerShutdownHooks(); + bot.start(); } + /** + * メトリクス関連コンポーネントを生成し起動する + * 公開無効時はNoOp実装を採用しnullを返さない + */ + private void initMetrics() { + MetricsConfig metricsConfig = configManager.getConfig().getMetricsConfig(); + + if (!metricsConfig.isEnabled()) { + this.metricsRegistry = MetricsRegistry.noop(); + this.ttsCountRecorder = new TTSCountRecorder(metricsRegistry); + logger.info("Prometheus metrics is disabled"); + return; + } + + PrometheusMetricsRegistry prometheusRegistry = new PrometheusMetricsRegistry(); + this.metricsRegistry = prometheusRegistry; + this.ttsCountRecorder = new TTSCountRecorder(metricsRegistry); + + try { + this.prometheusHttpExposer = new PrometheusHttpExposer(prometheusRegistry); + this.prometheusHttpExposer.start(metricsConfig.getBindAddress(), metricsConfig.getPort()); + logger.info("Prometheus metrics endpoint started on {}:{}/metrics", metricsConfig.getBindAddress(), metricsConfig.getPort()); + } catch (Exception e) { + logger.warn("Failed to start Prometheus HTTP exposer", e); + this.prometheusHttpExposer = null; + } + } + + /** + * シャットダウンフックを登録する + * メトリクスHTTPサーバなどライフサイクル管理対象を集約する + */ + private void registerShutdownHooks() { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + PrometheusHttpExposer exposer = this.prometheusHttpExposer; + if (exposer != null) { + exposer.stop(); + } + }, "prometheus-exposer-shutdown")); + } + public long getStartupTime() { return startupTime; } @@ -288,4 +347,14 @@ public Bot getBot() { public TTSCountRecorder getTTSCountRecorder() { return ttsCountRecorder; } + + /** + * メトリクスレジストリを取得 + * 公開無効時はNoOp実装が返るためnullにはならない + * + * @return メトリクスレジストリ + */ + public MetricsRegistry getMetricsRegistry() { + return metricsRegistry; + } } diff --git a/core/src/main/java/dev/felnull/itts/core/ITTSRuntimeUse.java b/core/src/main/java/dev/felnull/itts/core/ITTSRuntimeUse.java index a399808..6664d7c 100644 --- a/core/src/main/java/dev/felnull/itts/core/ITTSRuntimeUse.java +++ b/core/src/main/java/dev/felnull/itts/core/ITTSRuntimeUse.java @@ -5,6 +5,7 @@ import dev.felnull.itts.core.config.ConfigManager; import dev.felnull.itts.core.dict.DictionaryManager; import dev.felnull.itts.core.discord.Bot; +import dev.felnull.itts.core.metrics.MetricsRegistry; import dev.felnull.itts.core.tts.TTSCountRecorder; import dev.felnull.itts.core.tts.TTSManager; import dev.felnull.itts.core.voice.VoiceManager; @@ -93,4 +94,14 @@ default ITTSNetworkManager getNetworkManager() { default TTSCountRecorder getTTSCountRecorder() { return getITTSRuntime().getTTSCountRecorder(); } + + /** + * メトリクスレジストリを取得 + * 公開無効時もNoOp実装が返るためnullにはならない + * + * @return メトリクスレジストリ + */ + default MetricsRegistry getMetricsRegistry() { + return getITTSRuntime().getMetricsRegistry(); + } } diff --git a/core/src/main/java/dev/felnull/itts/core/config/Config.java b/core/src/main/java/dev/felnull/itts/core/config/Config.java index 5d68ede..6a733d0 100644 --- a/core/src/main/java/dev/felnull/itts/core/config/Config.java +++ b/core/src/main/java/dev/felnull/itts/core/config/Config.java @@ -82,4 +82,13 @@ public interface Config { * @return DB関係のコンフィグ */ DataBaseConfig getDataBaseConfig(); + + /** + * Prometheusメトリクスのコンフィグを取得 + * + * @return メトリクスコンフィグ + */ + default MetricsConfig getMetricsConfig() { + return MetricsConfig.DEFAULT; + } } diff --git a/core/src/main/java/dev/felnull/itts/core/config/MetricsConfig.java b/core/src/main/java/dev/felnull/itts/core/config/MetricsConfig.java new file mode 100644 index 0000000..7516723 --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/config/MetricsConfig.java @@ -0,0 +1,66 @@ +package dev.felnull.itts.core.config; + +import org.jetbrains.annotations.NotNull; + +/** + * Prometheusメトリクス公開のコンフィグ + */ +public interface MetricsConfig { + + /** + * デフォルトの有効状態 + */ + boolean DEFAULT_ENABLED = false; + + /** + * デフォルトのバインドアドレス + */ + String DEFAULT_BIND_ADDRESS = "127.0.0.1"; + + /** + * デフォルトのポート番号 + */ + int DEFAULT_PORT = 9095; + + /** + * デフォルトのコンフィグインスタンス + */ + MetricsConfig DEFAULT = new MetricsConfig() { + @Override + public boolean isEnabled() { + return DEFAULT_ENABLED; + } + + @Override + public @NotNull String getBindAddress() { + return DEFAULT_BIND_ADDRESS; + } + + @Override + public int getPort() { + return DEFAULT_PORT; + } + }; + + /** + * 有効かどうかを取得 + * + * @return 有効かどうか + */ + boolean isEnabled(); + + /** + * バインドアドレスを取得 + * + * @return バインドアドレス + */ + @NotNull + String getBindAddress(); + + /** + * ポート番号を取得 + * + * @return ポート番号 + */ + int getPort(); +} diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java b/core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java new file mode 100644 index 0000000..b3420c3 --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/metrics/MetricsRegistry.java @@ -0,0 +1,46 @@ +package dev.felnull.itts.core.metrics; + +import io.micrometer.core.instrument.Counter; +import org.jetbrains.annotations.NotNull; + +/** + * メトリクスレジストリの抽象 + * 実装はPrometheus版とNoOp版を切り替える + */ +public interface MetricsRegistry { + + /** + * BOT全体合計を表すサーバーIDの予約値 + */ + long GLOBAL_SERVER_ID = 0L; + + /** + * NoOp実装を返すファクトリ + * + * @return 何もしないMetricsRegistry + */ + @NotNull + static MetricsRegistry noop() { + return NoOpMetricsRegistry.INSTANCE; + } + + /** + * 文字数Counterをラベル付きで取得 + * + * @param botId BOTのID + * @param serverId サーバーID GLOBAL_SERVER_IDの場合はBOT全体 + * @return Counter + */ + @NotNull + Counter getOrCreateCharCounter(long botId, long serverId); + + /** + * メッセージ数Counterをラベル付きで取得 + * + * @param botId BOTのID + * @param serverId サーバーID GLOBAL_SERVER_IDの場合はBOT全体 + * @return Counter + */ + @NotNull + Counter getOrCreateMessageCounter(long botId, long serverId); +} diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/NoOpMetricsRegistry.java b/core/src/main/java/dev/felnull/itts/core/metrics/NoOpMetricsRegistry.java new file mode 100644 index 0000000..8f1b9f7 --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/metrics/NoOpMetricsRegistry.java @@ -0,0 +1,50 @@ +package dev.felnull.itts.core.metrics; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.noop.NoopCounter; +import org.jetbrains.annotations.NotNull; + +/** + * 何もしないMetricsRegistry実装 + * メトリクス公開無効時に利用しnullチェックを不要にする + */ +final class NoOpMetricsRegistry implements MetricsRegistry { + + /** + * 共有インスタンス + */ + static final NoOpMetricsRegistry INSTANCE = new NoOpMetricsRegistry(); + + /** + * 文字数Counterの共有NoOpインスタンス + */ + private static final Counter NOOP_CHAR_COUNTER = new NoopCounter( + new Meter.Id("itts_spoken_chars_total", Tags.empty(), null, null, Meter.Type.COUNTER)); + + /** + * メッセージ数Counterの共有NoOpインスタンス + */ + private static final Counter NOOP_MESSAGE_COUNTER = new NoopCounter( + new Meter.Id("itts_spoken_messages_total", Tags.empty(), null, null, Meter.Type.COUNTER)); + + /** + * コンストラクタ + * シングルトン用に外部からの生成を抑止する + */ + private NoOpMetricsRegistry() { + } + + @Override + @NotNull + public Counter getOrCreateCharCounter(long botId, long serverId) { + return NOOP_CHAR_COUNTER; + } + + @Override + @NotNull + public Counter getOrCreateMessageCounter(long botId, long serverId) { + return NOOP_MESSAGE_COUNTER; + } +} diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java b/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java new file mode 100644 index 0000000..d302eab --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusHttpExposer.java @@ -0,0 +1,104 @@ +package dev.felnull.itts.core.metrics; + +import com.sun.net.httpserver.HttpServer; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * PrometheusメトリクスをHTTPで公開するエクスポーザ + */ +public final class PrometheusHttpExposer { + + /** + * リクエスト処理スレッドプールのスレッド数 + */ + private static final int EXECUTOR_THREAD_COUNT = 2; + + /** + * Executor停止時の待機秒数 + */ + private static final int EXECUTOR_SHUTDOWN_TIMEOUT_SEC = 5; + + /** + * Prometheus形式のメトリクスレジストリ + */ + private final PrometheusMetricsRegistry metricsRegistry; + + /** + * 内部HTTPサーバー + */ + private HttpServer httpServer; + + /** + * リクエスト処理用スレッドプール + */ + private ExecutorService executor; + + /** + * コンストラクタ + * + * @param metricsRegistry Prometheus形式のメトリクスレジストリ + */ + public PrometheusHttpExposer(@NotNull PrometheusMetricsRegistry metricsRegistry) { + this.metricsRegistry = metricsRegistry; + } + + /** + * 起動する + * + * @param host バインドアドレス + * @param port ポート番号 + * @throws IOException 起動失敗時 + */ + public void start(@NotNull String host, int port) throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(host, port), 0); + server.createContext("/metrics", exchange -> { + String body = metricsRegistry.getRegistry().scrape(); + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", "text/plain; version=0.0.4; charset=utf-8"); + exchange.sendResponseHeaders(200, bytes.length); + try (OutputStream out = exchange.getResponseBody()) { + out.write(bytes); + } + }); + ExecutorService pool = Executors.newFixedThreadPool(EXECUTOR_THREAD_COUNT, + new BasicThreadFactory.Builder() + .namingPattern("prometheus-exposer-%d") + .daemon(true) + .build()); + server.setExecutor(pool); + server.start(); + this.httpServer = server; + this.executor = pool; + } + + /** + * 停止する + */ + public void stop() { + if (httpServer != null) { + httpServer.stop(0); + httpServer = null; + } + if (executor != null) { + executor.shutdown(); + try { + if (!executor.awaitTermination(EXECUTOR_SHUTDOWN_TIMEOUT_SEC, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + executor = null; + } + } +} diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusMetricsRegistry.java b/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusMetricsRegistry.java new file mode 100644 index 0000000..f27edce --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/metrics/PrometheusMetricsRegistry.java @@ -0,0 +1,127 @@ +package dev.felnull.itts.core.metrics; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; +import io.micrometer.core.instrument.binder.system.ProcessorMetrics; +import io.micrometer.prometheusmetrics.PrometheusConfig; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Prometheus形式のメトリクスレジストリ実装 + * Counterのキャッシュとシステム系メトリクスのバインドを行う + */ +public final class PrometheusMetricsRegistry implements MetricsRegistry { + + /** + * 文字数Counterのメトリクス名 + */ + private static final String CHAR_METRIC_NAME = "itts_spoken_chars_total"; + + /** + * 文字数Counterの説明 + */ + private static final String CHAR_METRIC_DESCRIPTION = "Total spoken characters delivered to TTS API"; + + /** + * メッセージ数Counterのメトリクス名 + */ + private static final String MESSAGE_METRIC_NAME = "itts_spoken_messages_total"; + + /** + * メッセージ数Counterの説明 + */ + private static final String MESSAGE_METRIC_DESCRIPTION = "Total spoken messages delivered to TTS API"; + + /** + * Prometheus形式のレジストリ + */ + private final PrometheusMeterRegistry registry; + + /** + * 起動時刻のミリ秒 + */ + private final long bootAt; + + /** + * 文字数Counterのキャッシュ + */ + private final ConcurrentMap charCounters = new ConcurrentHashMap<>(); + + /** + * メッセージ数Counterのキャッシュ + */ + private final ConcurrentMap messageCounters = new ConcurrentHashMap<>(); + + /** + * コンストラクタ + */ + public PrometheusMetricsRegistry() { + this.registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); + this.bootAt = System.currentTimeMillis(); + + Gauge.builder("itts_up", () -> 1.0d) + .description("Bot liveness indicator") + .register(registry); + + Gauge.builder("itts_uptime_seconds", () -> (System.currentTimeMillis() - bootAt) / 1000.0d) + .description("Bot uptime in seconds") + .register(registry); + + new JvmMemoryMetrics().bindTo(registry); + new JvmGcMetrics().bindTo(registry); + new ProcessorMetrics().bindTo(registry); + } + + /** + * Prometheusレジストリを取得 + * + * @return レジストリ + */ + @NotNull + public PrometheusMeterRegistry getRegistry() { + return registry; + } + + @Override + @NotNull + public Counter getOrCreateCharCounter(long botId, long serverId) { + return getOrCreate(charCounters, CHAR_METRIC_NAME, CHAR_METRIC_DESCRIPTION, botId, serverId); + } + + @Override + @NotNull + public Counter getOrCreateMessageCounter(long botId, long serverId) { + return getOrCreate(messageCounters, MESSAGE_METRIC_NAME, MESSAGE_METRIC_DESCRIPTION, botId, serverId); + } + + /** + * Counterをキャッシュから取得もしくは生成する + * + * @param cache Counterキャッシュ + * @param metricName メトリクス名 + * @param description メトリクスの説明 + * @param botId BOTのID + * @param serverId サーバーID GLOBAL_SERVER_IDの場合はBOT全体 + * @return Counter + */ + @NotNull + private Counter getOrCreate(@NotNull ConcurrentMap cache, + @NotNull String metricName, + @NotNull String description, + long botId, + long serverId) { + String serverTag = serverId == GLOBAL_SERVER_ID ? "global" : String.valueOf(serverId); + String key = botId + "|" + serverTag; + return cache.computeIfAbsent(key, k -> Counter.builder(metricName) + .description(description) + .tag("bot_id", String.valueOf(botId)) + .tag("server_id", serverTag) + .register(registry)); + } +} diff --git a/core/src/main/java/dev/felnull/itts/core/metrics/package-info.java b/core/src/main/java/dev/felnull/itts/core/metrics/package-info.java new file mode 100644 index 0000000..8ab66c6 --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/metrics/package-info.java @@ -0,0 +1,4 @@ +/** + * Prometheusメトリクスの収集と公開 + */ +package dev.felnull.itts.core.metrics; diff --git a/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java b/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java index 64ac100..ca97d74 100644 --- a/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java +++ b/core/src/main/java/dev/felnull/itts/core/tts/TTSCountRecorder.java @@ -1,6 +1,7 @@ package dev.felnull.itts.core.tts; import dev.felnull.itts.core.ITTSRuntimeUse; +import dev.felnull.itts.core.metrics.MetricsRegistry; import dev.felnull.itts.core.savedata.SaveDataManager; import dev.felnull.itts.core.savedata.repository.DataRepository; import dev.felnull.itts.core.savedata.repository.TTSCountData; @@ -14,6 +15,21 @@ */ public final class TTSCountRecorder implements ITTSRuntimeUse { + /** + * メトリクスレジストリ + * 公開無効時はNoOp実装が渡されるためnullにはならない + */ + private final MetricsRegistry metricsRegistry; + + /** + * コンストラクタ + * + * @param metricsRegistry メトリクスレジストリ + */ + public TTSCountRecorder(MetricsRegistry metricsRegistry) { + this.metricsRegistry = metricsRegistry; + } + /** * 読み上げ文字数を記録する * @@ -26,6 +42,15 @@ public void record(long botDiscordId, long guildDiscordId, int charCount) { return; } + try { + metricsRegistry.getOrCreateCharCounter(botDiscordId, guildDiscordId).increment(charCount); + metricsRegistry.getOrCreateMessageCounter(botDiscordId, guildDiscordId).increment(); + metricsRegistry.getOrCreateCharCounter(botDiscordId, MetricsRegistry.GLOBAL_SERVER_ID).increment(charCount); + metricsRegistry.getOrCreateMessageCounter(botDiscordId, MetricsRegistry.GLOBAL_SERVER_ID).increment(); + } catch (Throwable t) { + getITTSLogger().warn("Failed to update metrics counter", t); + } + CompletableFuture.runAsync(() -> writeToDatabase(botDiscordId, guildDiscordId, charCount), getAsyncExecutor()) .exceptionally(throwable -> { getITTSLogger().warn("Failed to record TTS count", throwable); diff --git a/selfhost/src/main/java/dev/felnull/itts/config/ConfigImpl.java b/selfhost/src/main/java/dev/felnull/itts/config/ConfigImpl.java index 704dd5f..0967ec6 100644 --- a/selfhost/src/main/java/dev/felnull/itts/config/ConfigImpl.java +++ b/selfhost/src/main/java/dev/felnull/itts/config/ConfigImpl.java @@ -5,6 +5,7 @@ import dev.felnull.itts.config.old.ConfigV0; import dev.felnull.itts.core.config.Config; import dev.felnull.itts.core.config.DataBaseConfig; +import dev.felnull.itts.core.config.MetricsConfig; import dev.felnull.itts.core.config.voicetype.VoiceTextConfig; import dev.felnull.itts.core.config.voicetype.VoicevoxConfig; import dev.felnull.itts.core.util.NameSerializableEnum; @@ -26,6 +27,7 @@ * @param coeirolnkConfig COEIROLNK コンフィグ * @param sharevoxConfig SHAREVOX コンフィグ * @param dataBaseConfig データベースコンフィグ + * @param metricsConfig Prometheusメトリクスコンフィグ */ public record ConfigImpl( String botToken, @@ -35,7 +37,8 @@ public record ConfigImpl( VoicevoxConfig voicevoxConfig, VoicevoxConfig coeirolnkConfig, VoicevoxConfig sharevoxConfig, - DataBaseConfig dataBaseConfig + DataBaseConfig dataBaseConfig, + MetricsConfig metricsConfig ) implements Config { /** @@ -52,6 +55,7 @@ public ConfigImpl load(JsonObject json5) { VoicevoxConfig coeirolnkConfig = VoicevoxConfigImpl.fromJson(Optional.ofNullable(json5.getObject("coeirolnk")).orElseGet(JsonObject::new)); VoicevoxConfig sharevoxConfig = VoicevoxConfigImpl.fromJson(Optional.ofNullable(json5.getObject("sharevox")).orElseGet(JsonObject::new)); DataBaseConfig dataBaseConfig = DataBaseConfigImpl.fromJson(Optional.ofNullable(json5.getObject("data_base")).orElseGet(JsonObject::new)); + MetricsConfig metricsConfig = MetricsConfigImpl.fromJson(Optional.ofNullable(json5.getObject("metrics")).orElseGet(JsonObject::new)); return new ConfigImpl( botToken, @@ -61,7 +65,8 @@ public ConfigImpl load(JsonObject json5) { voicevoxConfig, coeirolnkConfig, sharevoxConfig, - dataBaseConfig + dataBaseConfig, + metricsConfig ); } @@ -77,7 +82,8 @@ public ConfigImpl migrate(Object oldConfig) { new VoicevoxConfigImpl(configV0.voicevoxConfig().enable(), configV0.voicevoxConfig().apiUrls(), configV0.voicevoxConfig().checkTime()), new VoicevoxConfigImpl(configV0.coeirolnkConfig().enable(), configV0.coeirolnkConfig().apiUrls(), configV0.coeirolnkConfig().checkTime()), new VoicevoxConfigImpl(configV0.sharevoxConfig().enable(), configV0.sharevoxConfig().apiUrls(), configV0.sharevoxConfig().checkTime()), - new DataBaseConfigImpl() + new DataBaseConfigImpl(), + new MetricsConfigImpl() ); } }; @@ -96,7 +102,8 @@ public static ConfigImpl createInitialConfig() { new VoicevoxConfigImpl(), new VoicevoxConfigImpl(), new VoicevoxConfigImpl(), - new DataBaseConfigImpl() + new DataBaseConfigImpl(), + new MetricsConfigImpl() ); } @@ -112,6 +119,7 @@ public void writeToJson(JsonObject json5) { json5.put("coeirolnk", ((VoicevoxConfigImpl) this.coeirolnkConfig).toJson(), "COEIROLNKのコンフィグ"); json5.put("sharevox", ((VoicevoxConfigImpl) this.sharevoxConfig).toJson(), "SHAREVOXのコンフィグ"); json5.put("data_base", ((DataBaseConfigImpl) this.dataBaseConfig).toJson(), "データベースのコンフィグ"); + json5.put("metrics", ((MetricsConfigImpl) this.metricsConfig).toJson(), "Prometheusメトリクスのコンフィグ"); } @Override @@ -154,6 +162,11 @@ public DataBaseConfig getDataBaseConfig() { return dataBaseConfig; } + @Override + public MetricsConfig getMetricsConfig() { + return metricsConfig; + } + /** * VOICETEXTコンフィグの実装 * @@ -313,4 +326,52 @@ public JsonObject toJson() { return password; } } + + /** + * Prometheusメトリクスコンフィグの実装 + * + * @param enabled 有効かどうか + * @param bindAddress バインドアドレス + * @param port ポート番号 + */ + private record MetricsConfigImpl( + boolean enabled, + String bindAddress, + @Range(from = 0, to = 65535) int port + ) implements MetricsConfig { + + private MetricsConfigImpl() { + this(DEFAULT_ENABLED, DEFAULT_BIND_ADDRESS, DEFAULT_PORT); + } + + public static MetricsConfigImpl fromJson(JsonObject jo) { + boolean enabled = jo.getBoolean("enable", DEFAULT_ENABLED); + String bindAddress = Json5Utils.getStringOrElse(jo, "bind_address", DEFAULT_BIND_ADDRESS); + int port = jo.getInt("port", DEFAULT_PORT); + return new MetricsConfigImpl(enabled, bindAddress, port); + } + + public JsonObject toJson() { + JsonObject jo = new JsonObject(); + jo.put("enable", JsonPrimitive.of(enabled), "Prometheusメトリクス公開を有効にするかどうか"); + jo.put("bind_address", JsonPrimitive.of(bindAddress), "メトリクスHTTPサーバーのバインドアドレス"); + jo.put("port", new JsonPrimitive((long) port), "メトリクスHTTPサーバーのポート番号"); + return jo; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public @NotNull String getBindAddress() { + return bindAddress; + } + + @Override + public @Range(from = 0, to = 65535) int getPort() { + return port; + } + } }