add tracing opentelemetry

This commit is contained in:
mii443
2025-04-10 13:40:29 +09:00
parent 24c609aaf2
commit 8a1fa22074
17 changed files with 464 additions and 200 deletions

View File

@ -18,6 +18,12 @@ regex = "1"
tracing-subscriber = "0.3.19" tracing-subscriber = "0.3.19"
lru = "0.13.0" lru = "0.13.0"
tracing = "0.1.41" tracing = "0.1.41"
opentelemetry_sdk = { version = "0.29.0", features = ["trace"] }
opentelemetry = "0.29.1"
opentelemetry-semantic-conventions = "0.29.0"
opentelemetry-otlp = { version = "0.29.0", features = ["grpc-tonic"] }
opentelemetry-stdout = "0.29.0"
tracing-opentelemetry = "0.30.0"
[dependencies.uuid] [dependencies.uuid]
version = "0.8" version = "0.8"

View File

@ -1,6 +1,8 @@
use serenity::{ use serenity::{
all::{ all::{
ButtonStyle, CommandInteraction, CreateActionRow, CreateButton, CreateInteractionResponse, CreateInteractionResponseMessage, CreateSelectMenu, CreateSelectMenuKind, CreateSelectMenuOption ButtonStyle, CommandInteraction, CreateActionRow, CreateButton, CreateInteractionResponse,
CreateInteractionResponseMessage, CreateSelectMenu, CreateSelectMenuKind,
CreateSelectMenuOption,
}, },
prelude::Context, prelude::Context,
}; };
@ -10,6 +12,7 @@ use crate::{
tts::tts_type::TTSType, tts::tts_type::TTSType,
}; };
#[tracing::instrument]
pub async fn config_command( pub async fn config_command(
ctx: &Context, ctx: &Context,
command: &CommandInteraction, command: &CommandInteraction,
@ -38,53 +41,59 @@ pub async fn config_command(
let tts_type = config.tts_type.unwrap_or(TTSType::GCP); let tts_type = config.tts_type.unwrap_or(TTSType::GCP);
let engine_select = CreateActionRow::SelectMenu( let engine_select = CreateActionRow::SelectMenu(
CreateSelectMenu::new("TTS_CONFIG_ENGINE", CreateSelectMenuKind::String { options: vec![ CreateSelectMenu::new(
CreateSelectMenuOption::new("Google TTS", "TTS_CONFIG_ENGINE_SELECTED_GOOGLE") "TTS_CONFIG_ENGINE",
.default_selection(tts_type == TTSType::GCP), CreateSelectMenuKind::String {
CreateSelectMenuOption::new("VOICEVOX", "TTS_CONFIG_ENGINE_SELECTED_VOICEVOX") options: vec![
.default_selection(tts_type == TTSType::VOICEVOX) CreateSelectMenuOption::new("Google TTS", "TTS_CONFIG_ENGINE_SELECTED_GOOGLE")
] }).placeholder("読み上げAPIを選択") .default_selection(tts_type == TTSType::GCP),
CreateSelectMenuOption::new("VOICEVOX", "TTS_CONFIG_ENGINE_SELECTED_VOICEVOX")
.default_selection(tts_type == TTSType::VOICEVOX),
],
},
)
.placeholder("読み上げAPIを選択"),
); );
let server_button = CreateActionRow::Buttons(vec![ let server_button = CreateActionRow::Buttons(vec![CreateButton::new("TTS_CONFIG_SERVER")
CreateButton::new("TTS_CONFIG_SERVER") .label("サーバー設定")
.label("サーバー設定") .style(ButtonStyle::Primary)]);
.style(ButtonStyle::Primary)
]);
let mut components = vec![engine_select, server_button]; let mut components = vec![engine_select, server_button];
for (index, speaker_chunk) in voicevox_speakers[0..24].chunks(25).enumerate() { for (index, speaker_chunk) in voicevox_speakers[0..24].chunks(25).enumerate() {
let mut options = Vec::new(); let mut options = Vec::new();
for (name, id) in speaker_chunk { for (name, id) in speaker_chunk {
options.push( options.push(
CreateSelectMenuOption::new( CreateSelectMenuOption::new(
name, name,
format!("TTS_CONFIG_VOICEVOX_SPEAKER_SELECTED_{}", id) format!("TTS_CONFIG_VOICEVOX_SPEAKER_SELECTED_{}", id),
).default_selection(*id == voicevox_speaker) )
.default_selection(*id == voicevox_speaker),
); );
} }
components.push( components.push(CreateActionRow::SelectMenu(
CreateActionRow::SelectMenu( CreateSelectMenu::new(
CreateSelectMenu::new( format!("TTS_CONFIG_VOICEVOX_SPEAKER_{}", index),
format!("TTS_CONFIG_VOICEVOX_SPEAKER_{}", index), CreateSelectMenuKind::String { options },
CreateSelectMenuKind::String { options }
).placeholder("VOICEVOX Speakerを指定")
) )
); .placeholder("VOICEVOX Speakerを指定"),
));
} }
command command
.create_response(&ctx.http, .create_response(
&ctx.http,
CreateInteractionResponse::Message( CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new() CreateInteractionResponseMessage::new()
.content("読み上げ設定") .content("読み上げ設定")
.components(components) .components(components)
.ephemeral(true) .ephemeral(true),
)) ),
)
.await?; .await?;
Ok(()) Ok(())
} }

View File

@ -5,17 +5,19 @@ use serenity::{
model::prelude::UserId, model::prelude::UserId,
prelude::Context, prelude::Context,
}; };
use tracing::info;
use crate::{ use crate::{
data::{TTSClientData, TTSData}, data::{TTSClientData, TTSData},
tts::instance::TTSInstance, tts::instance::TTSInstance,
}; };
#[tracing::instrument]
pub async fn setup_command( pub async fn setup_command(
ctx: &Context, ctx: &Context,
command: &CommandInteraction, command: &CommandInteraction,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
println!("Received event"); info!("Received event");
if command.guild_id.is_none() { if command.guild_id.is_none() {
command command
@ -29,7 +31,7 @@ pub async fn setup_command(
return Ok(()); return Ok(());
} }
println!("Fetching guild cache"); info!("Fetching guild cache");
let guild_id = command.guild_id.unwrap(); let guild_id = command.guild_id.unwrap();
let guild = guild_id.to_guild_cached(&ctx.cache).unwrap().clone(); let guild = guild_id.to_guild_cached(&ctx.cache).unwrap().clone();

View File

@ -1,7 +1,4 @@
use crate::{ use crate::{database::database::Database, tts::tts::TTS};
database::database::Database,
tts::tts::TTS,
};
use serenity::{ use serenity::{
futures::lock::Mutex, futures::lock::Mutex,
model::id::GuildId, model::id::GuildId,

View File

@ -5,6 +5,7 @@ use crate::tts::{
use super::{dictionary::Dictionary, server_config::ServerConfig, user_config::UserConfig}; use super::{dictionary::Dictionary, server_config::ServerConfig, user_config::UserConfig};
use redis::Commands; use redis::Commands;
#[derive(Debug, Clone)]
pub struct Database { pub struct Database {
pub client: redis::Client, pub client: redis::Client,
} }
@ -14,6 +15,7 @@ impl Database {
Self { client } Self { client }
} }
#[tracing::instrument]
pub async fn get_server_config( pub async fn get_server_config(
&mut self, &mut self,
server_id: u64, server_id: u64,
@ -32,6 +34,7 @@ impl Database {
} }
} }
#[tracing::instrument]
pub async fn get_user_config( pub async fn get_user_config(
&mut self, &mut self,
user_id: u64, user_id: u64,
@ -50,6 +53,7 @@ impl Database {
} }
} }
#[tracing::instrument]
pub async fn set_server_config( pub async fn set_server_config(
&mut self, &mut self,
server_id: u64, server_id: u64,
@ -64,6 +68,7 @@ impl Database {
Ok(()) Ok(())
} }
#[tracing::instrument]
pub async fn set_user_config( pub async fn set_user_config(
&mut self, &mut self,
user_id: u64, user_id: u64,
@ -78,20 +83,25 @@ impl Database {
Ok(()) Ok(())
} }
#[tracing::instrument]
pub async fn set_default_server_config(&mut self, server_id: u64) -> redis::RedisResult<()> { pub async fn set_default_server_config(&mut self, server_id: u64) -> redis::RedisResult<()> {
let config = ServerConfig { let config = ServerConfig {
dictionary: Dictionary::new(), dictionary: Dictionary::new(),
autostart_channel_id: None, autostart_channel_id: None,
}; };
self.client.get_connection().unwrap().set::<String, String, ()>( self.client
format!("discord_server:{}", server_id), .get_connection()
serde_json::to_string(&config).unwrap(), .unwrap()
)?; .set::<String, String, ()>(
format!("discord_server:{}", server_id),
serde_json::to_string(&config).unwrap(),
)?;
Ok(()) Ok(())
} }
#[tracing::instrument]
pub async fn set_default_user_config(&mut self, user_id: u64) -> redis::RedisResult<()> { pub async fn set_default_user_config(&mut self, user_id: u64) -> redis::RedisResult<()> {
let voice_selection = VoiceSelectionParams { let voice_selection = VoiceSelectionParams {
languageCode: String::from("ja-JP"), languageCode: String::from("ja-JP"),
@ -107,14 +117,18 @@ impl Database {
voicevox_speaker: Some(1), voicevox_speaker: Some(1),
}; };
self.client.get_connection().unwrap().set::<String, String, ()>( self.client
format!("discord_user:{}", user_id), .get_connection()
serde_json::to_string(&config).unwrap(), .unwrap()
)?; .set::<String, String, ()>(
format!("discord_user:{}", user_id),
serde_json::to_string(&config).unwrap(),
)?;
Ok(()) Ok(())
} }
#[tracing::instrument]
pub async fn get_server_config_or_default( pub async fn get_server_config_or_default(
&mut self, &mut self,
server_id: u64, server_id: u64,
@ -129,6 +143,7 @@ impl Database {
} }
} }
#[tracing::instrument]
pub async fn get_user_config_or_default( pub async fn get_user_config_or_default(
&mut self, &mut self,
user_id: u64, user_id: u64,

View File

@ -8,19 +8,31 @@ use crate::{
tts::tts_type::TTSType, tts::tts_type::TTSType,
}; };
use serenity::{ use serenity::{
all::{ActionRowComponent, ButtonStyle, ComponentInteractionDataKind, CreateActionRow, CreateButton, CreateEmbed, CreateInputText, CreateInteractionResponse, CreateInteractionResponseMessage, CreateModal, CreateSelectMenu, CreateSelectMenuKind, CreateSelectMenuOption, InputTextStyle}, async_trait, client::{Context, EventHandler}, model::{ all::{
application::Interaction, channel::Message, gateway::Ready, prelude::ChannelType, voice::VoiceState ActionRowComponent, ButtonStyle, ComponentInteractionDataKind, CreateActionRow,
} CreateButton, CreateEmbed, CreateInputText, CreateInteractionResponse,
CreateInteractionResponseMessage, CreateModal, CreateSelectMenu, CreateSelectMenuKind,
CreateSelectMenuOption, InputTextStyle,
},
async_trait,
client::{Context, EventHandler},
model::{
application::Interaction, channel::Message, gateway::Ready, prelude::ChannelType,
voice::VoiceState,
},
}; };
#[derive(Clone, Debug)]
pub struct Handler; pub struct Handler;
#[async_trait] #[async_trait]
impl EventHandler for Handler { impl EventHandler for Handler {
#[tracing::instrument]
async fn message(&self, ctx: Context, message: Message) { async fn message(&self, ctx: Context, message: Message) {
events::message_receive::message(ctx, message).await events::message_receive::message(ctx, message).await
} }
#[tracing::instrument]
async fn ready(&self, ctx: Context, ready: Ready) { async fn ready(&self, ctx: Context, ready: Ready) {
events::ready::ready(ctx, ready).await events::ready::ready(ctx, ready).await
} }
@ -95,10 +107,15 @@ impl EventHandler for Handler {
.await .await
.unwrap(); .unwrap();
modal modal
.create_response(&ctx.http, CreateInteractionResponse::UpdateMessage(CreateInteractionResponseMessage::new().content(format!( .create_response(
"辞書を追加しました\n名前: {}\n変換元: {}\n変換後: {}", &ctx.http,
rule_name, from, to CreateInteractionResponse::UpdateMessage(
)))) CreateInteractionResponseMessage::new().content(format!(
"辞書を追加しました\n名前: {}\n変換元: {}\n変換後: {}",
rule_name, from, to
)),
),
)
.await .await
.unwrap(); .unwrap();
} }
@ -106,12 +123,16 @@ impl EventHandler for Handler {
if let Some(message_component) = interaction.message_component() { if let Some(message_component) = interaction.message_component() {
match &*message_component.data.custom_id { match &*message_component.data.custom_id {
"TTS_CONFIG_SERVER_REMOVE_DICTIONARY_MENU" => { "TTS_CONFIG_SERVER_REMOVE_DICTIONARY_MENU" => {
let i = usize::from_str_radix(&match message_component.data.kind { let i = usize::from_str_radix(
ComponentInteractionDataKind::StringSelect { ref values, .. } => { &match message_component.data.kind {
values[0].clone() ComponentInteractionDataKind::StringSelect { ref values, .. } => {
} values[0].clone()
_ => panic!("Cannot get index"), }
}, 10).unwrap(); _ => panic!("Cannot get index"),
},
10,
)
.unwrap();
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
let mut config = { let mut config = {
@ -141,8 +162,13 @@ impl EventHandler for Handler {
} }
message_component message_component
.create_response(&ctx, .create_response(
CreateInteractionResponse::UpdateMessage(CreateInteractionResponseMessage::new().content("辞書を削除しました"))) &ctx,
CreateInteractionResponse::UpdateMessage(
CreateInteractionResponseMessage::new()
.content("辞書を削除しました"),
),
)
.await .await
.unwrap(); .unwrap();
} }
@ -163,38 +189,40 @@ impl EventHandler for Handler {
}; };
message_component message_component
.create_response(&ctx.http, .create_response(
&ctx.http,
CreateInteractionResponse::UpdateMessage( CreateInteractionResponse::UpdateMessage(
CreateInteractionResponseMessage::new() CreateInteractionResponseMessage::new()
.content("削除する辞書内容を選択してください") .content("削除する辞書内容を選択してください")
.components(vec![ .components(vec![CreateActionRow::SelectMenu(
CreateActionRow::SelectMenu( CreateSelectMenu::new(
CreateSelectMenu::new( "TTS_CONFIG_SERVER_REMOVE_DICTIONARY_MENU",
"TTS_CONFIG_SERVER_REMOVE_DICTIONARY_MENU", CreateSelectMenuKind::String {
CreateSelectMenuKind::String { options: {
options: { let mut options = vec![];
let mut options = vec![]; for (i, rule) in
for (i, rule) in config config.dictionary.rules.iter().enumerate()
.dictionary {
.rules let option = CreateSelectMenuOption::new(
.iter() rule.id.clone(),
.enumerate() i.to_string(),
{ )
let option = CreateSelectMenuOption::new(rule.id.clone(), i.to_string()).description(format!( .description(format!(
"{} -> {}", "{} -> {}",
rule.rule.clone(), rule.rule.clone(),
rule.to.clone() rule.to.clone()
)); ));
options.push(option); options.push(option);
}
options
} }
}) options
.max_values(1) },
.min_values(0) },
) )
]) .max_values(1)
)) .min_values(0),
)]),
),
)
.await .await
.unwrap(); .unwrap();
} }
@ -214,12 +242,11 @@ impl EventHandler for Handler {
}; };
message_component message_component
.create_response(&ctx.http, CreateInteractionResponse::UpdateMessage( .create_response(
CreateInteractionResponseMessage::new() &ctx.http,
.content("") CreateInteractionResponse::UpdateMessage(
.embed(CreateEmbed::new() CreateInteractionResponseMessage::new().content("").embed(
.title("辞書一覧") CreateEmbed::new().title("辞書一覧").fields({
.fields({
let mut fields = vec![]; let mut fields = vec![];
for rule in config.dictionary.rules { for rule in config.dictionary.rules {
let field = ( let field = (
@ -230,14 +257,17 @@ impl EventHandler for Handler {
fields.push(field); fields.push(field);
} }
fields fields
})) }),
)) ),
),
)
.await .await
.unwrap(); .unwrap();
} }
"TTS_CONFIG_SERVER_ADD_DICTIONARY_BUTTON" => { "TTS_CONFIG_SERVER_ADD_DICTIONARY_BUTTON" => {
message_component message_component
.create_response(&ctx.http, .create_response(
&ctx.http,
CreateInteractionResponse::Modal( CreateInteractionResponse::Modal(
CreateModal::new("TTS_CONFIG_SERVER_ADD_DICTIONARY", "辞書追加") CreateModal::new("TTS_CONFIG_SERVER_ADD_DICTIONARY", "辞書追加")
.components({ .components({
@ -246,29 +276,30 @@ impl EventHandler for Handler {
CreateInputText::new( CreateInputText::new(
InputTextStyle::Short, InputTextStyle::Short,
"rule_name", "rule_name",
"辞書名" "辞書名",
) )
.required(true) .required(true),
), ),
CreateActionRow::InputText( CreateActionRow::InputText(
CreateInputText::new( CreateInputText::new(
InputTextStyle::Paragraph, InputTextStyle::Paragraph,
"from", "from",
"変換元(正規表現)" "変換元(正規表現)",
) )
.required(true) .required(true),
), ),
CreateActionRow::InputText( CreateActionRow::InputText(
CreateInputText::new( CreateInputText::new(
InputTextStyle::Short, InputTextStyle::Short,
"to", "to",
"変換先" "変換先",
) )
.required(true) .required(true),
) ),
] ]
}) }),
)) ),
)
.await .await
.unwrap(); .unwrap();
} }
@ -278,7 +309,13 @@ impl EventHandler for Handler {
if values.len() == 0 { if values.len() == 0 {
None None
} else { } else {
Some(u64::from_str_radix(&values[0].strip_prefix("SET_AUTOSTART_CHANNEL_").unwrap(), 10).unwrap()) Some(
u64::from_str_radix(
&values[0].strip_prefix("SET_AUTOSTART_CHANNEL_").unwrap(),
10,
)
.unwrap(),
)
} }
} }
_ => panic!("Cannot get index"), _ => panic!("Cannot get index"),
@ -303,7 +340,13 @@ impl EventHandler for Handler {
}; };
message_component message_component
.create_response(&ctx.http, CreateInteractionResponse::UpdateMessage(CreateInteractionResponseMessage::new().content("自動参加チャンネルを設定しました。"))) .create_response(
&ctx.http,
CreateInteractionResponse::UpdateMessage(
CreateInteractionResponseMessage::new()
.content("自動参加チャンネルを設定しました。"),
),
)
.await .await
.unwrap(); .unwrap();
} }
@ -336,70 +379,82 @@ impl EventHandler for Handler {
if channel.kind != ChannelType::Voice { if channel.kind != ChannelType::Voice {
continue; continue;
} }
let description = channel.topic.unwrap_or_else(|| String::from("No topic provided.")); let description = channel
.topic
.unwrap_or_else(|| String::from("No topic provided."));
let option = CreateSelectMenuOption::new( let option = CreateSelectMenuOption::new(
&channel.name, &channel.name,
format!("SET_AUTOSTART_CHANNEL_{}", id.get()) format!("SET_AUTOSTART_CHANNEL_{}", id.get()),
) )
.description(description) .description(description)
.default_selection(channel.id.get() == autostart_channel_id); .default_selection(channel.id.get() == autostart_channel_id);
options.push(option); options.push(option);
} }
message_component message_component
.create_response(&ctx.http, .create_response(
&ctx.http,
CreateInteractionResponse::UpdateMessage( CreateInteractionResponse::UpdateMessage(
CreateInteractionResponseMessage::new() CreateInteractionResponseMessage::new()
.content("自動参加チャンネル設定") .content("自動参加チャンネル設定")
.components(vec![ .components(vec![CreateActionRow::SelectMenu(
CreateActionRow::SelectMenu( CreateSelectMenu::new(
CreateSelectMenu::new( "SET_AUTOSTART_CHANNEL",
"SET_AUTOSTART_CHANNEL", CreateSelectMenuKind::String { options },
CreateSelectMenuKind::String { options }
)
.min_values(0)
.max_values(1)
) )
]) .min_values(0)
)) .max_values(1),
)]),
),
)
.await .await
.unwrap(); .unwrap();
}, }
"TTS_CONFIG_SERVER" => { "TTS_CONFIG_SERVER" => {
message_component message_component
.create_response(&ctx.http, .create_response(
&ctx.http,
CreateInteractionResponse::UpdateMessage( CreateInteractionResponse::UpdateMessage(
CreateInteractionResponseMessage::new() CreateInteractionResponseMessage::new()
.content("サーバー設定") .content("サーバー設定")
.components(vec![ .components(vec![CreateActionRow::Buttons(vec![
CreateActionRow::Buttons(vec![ CreateButton::new(
CreateButton::new("TTS_CONFIG_SERVER_ADD_DICTIONARY_BUTTON") "TTS_CONFIG_SERVER_ADD_DICTIONARY_BUTTON",
.label("辞書を追加") )
.style(ButtonStyle::Primary), .label("辞書を追加")
CreateButton::new("TTS_CONFIG_SERVER_REMOVE_DICTIONARY_BUTTON") .style(ButtonStyle::Primary),
.label("辞書を削除") CreateButton::new(
.style(ButtonStyle::Danger), "TTS_CONFIG_SERVER_REMOVE_DICTIONARY_BUTTON",
CreateButton::new("TTS_CONFIG_SERVER_SHOW_DICTIONARY_BUTTON") )
.label("辞書一覧") .label("辞書を削除")
.style(ButtonStyle::Primary), .style(ButtonStyle::Danger),
CreateButton::new("TTS_CONFIG_SERVER_SET_AUTOSTART_CHANNEL") CreateButton::new(
.label("自動参加チャンネル") "TTS_CONFIG_SERVER_SHOW_DICTIONARY_BUTTON",
.style(ButtonStyle::Primary) )
]) .label("辞書一覧")
]) .style(ButtonStyle::Primary),
)) CreateButton::new(
"TTS_CONFIG_SERVER_SET_AUTOSTART_CHANNEL",
)
.label("自動参加チャンネル")
.style(ButtonStyle::Primary),
])]),
),
)
.await .await
.unwrap(); .unwrap();
} }
_ => {} _ => {}
} }
match message_component.data.kind { match message_component.data.kind {
ComponentInteractionDataKind::StringSelect { ref values, .. } if !values.is_empty() => { ComponentInteractionDataKind::StringSelect { ref values, .. }
if !values.is_empty() =>
{
let res = &values[0].clone(); let res = &values[0].clone();
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
let mut config = { let mut config = {
let database = data_read let database = data_read
.get::<DatabaseClientData>() .get::<DatabaseClientData>()
@ -412,10 +467,10 @@ impl EventHandler for Handler {
.unwrap() .unwrap()
.unwrap() .unwrap()
}; };
let mut config_changed = false; let mut config_changed = false;
let mut voicevox_changed = false; let mut voicevox_changed = false;
match res.as_str() { match res.as_str() {
"TTS_CONFIG_ENGINE_SELECTED_GOOGLE" => { "TTS_CONFIG_ENGINE_SELECTED_GOOGLE" => {
config.tts_type = Some(TTSType::GCP); config.tts_type = Some(TTSType::GCP);
@ -431,14 +486,14 @@ impl EventHandler for Handler {
.strip_prefix("TTS_CONFIG_VOICEVOX_SPEAKER_SELECTED_") .strip_prefix("TTS_CONFIG_VOICEVOX_SPEAKER_SELECTED_")
.and_then(|id_str| id_str.parse::<i64>().ok()) .and_then(|id_str| id_str.parse::<i64>().ok())
.expect("Invalid speaker ID format"); .expect("Invalid speaker ID format");
config.voicevox_speaker = Some(speaker_id); config.voicevox_speaker = Some(speaker_id);
config_changed = true; config_changed = true;
voicevox_changed = true; voicevox_changed = true;
} }
} }
} }
if config_changed { if config_changed {
let database = data_read let database = data_read
.get::<DatabaseClientData>() .get::<DatabaseClientData>()
@ -449,26 +504,29 @@ impl EventHandler for Handler {
.set_user_config(message_component.user.id.get(), config.clone()) .set_user_config(message_component.user.id.get(), config.clone())
.await .await
.unwrap(); .unwrap();
let response_content = if voicevox_changed && config.tts_type.unwrap_or(TTSType::GCP) == TTSType::GCP { let response_content = if voicevox_changed
&& config.tts_type.unwrap_or(TTSType::GCP) == TTSType::GCP
{
"設定しました\nこの音声を使うにはAPIをGoogleからVOICEVOXに変更する必要があります。" "設定しました\nこの音声を使うにはAPIをGoogleからVOICEVOXに変更する必要があります。"
} else { } else {
"設定しました" "設定しました"
}; };
message_component message_component
.create_response(&ctx.http, .create_response(
&ctx.http,
CreateInteractionResponse::Message( CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new() CreateInteractionResponseMessage::new()
.content(response_content) .content(response_content)
.ephemeral(true) .ephemeral(true),
)) ),
)
.await .await
.unwrap(); .unwrap();
} }
},
_ => {
} }
_ => {}
} }
} }
} }

View File

@ -1,20 +1,33 @@
use serenity::{ use serenity::{
all::{Command, CommandOptionType, CreateCommand, CreateCommandOption}, model::prelude::Ready, prelude::Context all::{Command, CommandOptionType, CreateCommand, CreateCommandOption},
model::prelude::Ready,
prelude::Context,
}; };
use tracing::info;
#[tracing::instrument]
pub async fn ready(ctx: Context, ready: Ready) { pub async fn ready(ctx: Context, ready: Ready) {
println!("{} is connected!", ready.user.name); info!("{} is connected!", ready.user.name);
Command::set_global_commands(&ctx.http, vec![ Command::set_global_commands(
CreateCommand::new("stop").description("Stop tts"), &ctx.http,
CreateCommand::new("setup").description("Setup tts").set_options(vec![ vec![
CreateCommandOption::new(CommandOptionType::String, "mode", "TTS channel") CreateCommand::new("stop").description("Stop tts"),
.add_string_choice("Text Channel", "TEXT_CHANNEL") CreateCommand::new("setup")
.add_string_choice("New Thread", "NEW_THREAD") .description("Setup tts")
.add_string_choice("Voice Channel", "VOICE_CHANNEL") .set_options(vec![CreateCommandOption::new(
.required(false) CommandOptionType::String,
]), "mode",
CreateCommand::new("config").description("Config"), "TTS channel",
CreateCommand::new("skip").description("skip tts message"), )
]).await.unwrap(); .add_string_choice("Text Channel", "TEXT_CHANNEL")
.add_string_choice("New Thread", "NEW_THREAD")
.add_string_choice("Voice Channel", "VOICE_CHANNEL")
.required(false)]),
CreateCommand::new("config").description("Config"),
CreateCommand::new("skip").description("skip tts message"),
],
)
.await
.unwrap();
} }

View File

@ -1,6 +1,6 @@
use serenity::model::{ use serenity::model::{
guild::{Member, PartialMember},
user::User, user::User,
guild::{Member, PartialMember}
}; };
pub trait ReadName { pub trait ReadName {
@ -15,7 +15,9 @@ impl ReadName for Member {
impl ReadName for PartialMember { impl ReadName for PartialMember {
fn read_name(&self) -> String { fn read_name(&self) -> String {
self.nick.clone().unwrap_or(self.user.as_ref().unwrap().display_name().to_string()) self.nick
.clone()
.unwrap_or(self.user.as_ref().unwrap().display_name().to_string())
} }
} }
@ -23,4 +25,4 @@ impl ReadName for User {
fn read_name(&self) -> String { fn read_name(&self) -> String {
self.display_name().to_string() self.display_name().to_string()
} }
} }

View File

@ -4,7 +4,9 @@ use serenity::{model::prelude::Message, prelude::Context};
use songbird::input::cached::Compressed; use songbird::input::cached::Compressed;
use crate::{ use crate::{
data::{DatabaseClientData, TTSClientData}, implement::member_name::ReadName, tts::{ data::{DatabaseClientData, TTSClientData},
implement::member_name::ReadName,
tts::{
gcp_tts::structs::{ gcp_tts::structs::{
audio_config::AudioConfig, synthesis_input::SynthesisInput, audio_config::AudioConfig, synthesis_input::SynthesisInput,
synthesize_request::SynthesizeRequest, synthesize_request::SynthesizeRequest,
@ -12,7 +14,7 @@ use crate::{
instance::TTSInstance, instance::TTSInstance,
message::TTSMessage, message::TTSMessage,
tts_type::TTSType, tts_type::TTSType,
} },
}; };
#[async_trait] #[async_trait]
@ -48,7 +50,11 @@ impl TTSMessage for Message {
let member = self.member.clone(); let member = self.member.clone();
let name = if let Some(_) = member { let name = if let Some(_) = member {
let guild = ctx.cache.guild(self.guild_id.unwrap()).unwrap().clone(); let guild = ctx.cache.guild(self.guild_id.unwrap()).unwrap().clone();
guild.member(&ctx.http, self.author.id).await.unwrap().read_name() guild
.member(&ctx.http, self.author.id)
.await
.unwrap()
.read_name()
} else { } else {
self.author.read_name() self.author.read_name()
}; };
@ -58,7 +64,11 @@ impl TTSMessage for Message {
let member = self.member.clone(); let member = self.member.clone();
let name = if let Some(_) = member { let name = if let Some(_) = member {
let guild = ctx.cache.guild(self.guild_id.unwrap()).unwrap().clone(); let guild = ctx.cache.guild(self.guild_id.unwrap()).unwrap().clone();
guild.member(&ctx.http, self.author.id).await.unwrap().read_name() guild
.member(&ctx.http, self.author.id)
.await
.unwrap()
.read_name()
} else { } else {
self.author.read_name() self.author.read_name()
}; };

View File

@ -5,6 +5,7 @@ mod database;
mod event_handler; mod event_handler;
mod events; mod events;
mod implement; mod implement;
mod trace;
mod tts; mod tts;
use std::{collections::HashMap, env, sync::Arc}; use std::{collections::HashMap, env, sync::Arc};
@ -14,9 +15,14 @@ use data::{DatabaseClientData, TTSClientData, TTSData};
use database::database::Database; use database::database::Database;
use event_handler::Handler; use event_handler::Handler;
use serenity::{ use serenity::{
all::{standard::Configuration, ApplicationId}, client::Client, framework::StandardFramework, futures::lock::Mutex, prelude::{GatewayIntents, RwLock} all::{standard::Configuration, ApplicationId},
client::Client,
framework::StandardFramework,
futures::lock::Mutex,
prelude::{GatewayIntents, RwLock},
}; };
use tracing::Level; use trace::init_tracing_subscriber;
use tracing::info;
use tts::{gcp_tts::gcp_tts::GCPTTS, tts::TTS, voicevox::voicevox::VOICEVOX}; use tts::{gcp_tts::gcp_tts::GCPTTS, tts::TTS, voicevox::voicevox::VOICEVOX};
use songbird::SerenityInit; use songbird::SerenityInit;
@ -43,7 +49,7 @@ async fn create_client(prefix: &str, token: &str, id: u64) -> Result<Client, ser
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
tracing_subscriber::fmt().with_max_level(Level::INFO).init(); let _guard = init_tracing_subscriber();
// Load config // Load config
let config = { let config = {
let config = std::fs::read_to_string("./config.toml"); let config = std::fs::read_to_string("./config.toml");
@ -92,6 +98,8 @@ async fn main() {
data.insert::<DatabaseClientData>(Arc::new(Mutex::new(database_client))); data.insert::<DatabaseClientData>(Arc::new(Mutex::new(database_client)));
} }
info!("Bot initialized.");
// Run client // Run client
if let Err(why) = client.start().await { if let Err(why) = client.start().await {
println!("Client error: {:?}", why); println!("Client error: {:?}", why);

116
src/trace.rs Normal file
View File

@ -0,0 +1,116 @@
use opentelemetry::{
global,
trace::{SamplingDecision, SamplingResult, TraceContextExt, TraceState, TracerProvider as _},
KeyValue,
};
use opentelemetry_sdk::{
metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider},
trace::{RandomIdGenerator, SdkTracerProvider, ShouldSample},
Resource,
};
use opentelemetry_semantic_conventions::{
resource::{SERVICE_NAME, SERVICE_VERSION},
SCHEMA_URL,
};
use tracing::Level;
use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[derive(Debug, Clone)]
struct FilterSampler;
impl ShouldSample for FilterSampler {
fn should_sample(
&self,
parent_context: Option<&opentelemetry::Context>,
_trace_id: opentelemetry::TraceId,
name: &str,
_span_kind: &opentelemetry::trace::SpanKind,
_attributes: &[KeyValue],
_links: &[opentelemetry::trace::Link],
) -> opentelemetry::trace::SamplingResult {
let decision = if name == "dispatch" || name == "recv_event" {
SamplingDecision::Drop
} else {
SamplingDecision::RecordAndSample
};
SamplingResult {
decision,
attributes: vec![],
trace_state: match parent_context {
Some(ctx) => ctx.span().span_context().trace_state().clone(),
None => TraceState::default(),
},
}
}
}
fn resource() -> Resource {
Resource::builder().with_service_name("ncb-tts-r2").build()
}
fn init_meter_provider() -> SdkMeterProvider {
let exporter = opentelemetry_otlp::MetricExporter::builder()
.with_http()
.with_temporality(opentelemetry_sdk::metrics::Temporality::default())
.build()
.unwrap();
let reader = PeriodicReader::builder(exporter)
.with_interval(std::time::Duration::from_secs(5))
.build();
let stdout_reader =
PeriodicReader::builder(opentelemetry_stdout::MetricExporter::default()).build();
let meter_provider = MeterProviderBuilder::default()
.with_resource(resource())
.with_reader(reader)
.with_reader(stdout_reader)
.build();
global::set_meter_provider(meter_provider.clone());
meter_provider
}
fn init_tracer_provider() -> SdkTracerProvider {
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_http()
.build()
.unwrap();
SdkTracerProvider::builder()
.with_sampler(FilterSampler)
.with_id_generator(RandomIdGenerator::default())
.with_resource(resource())
.with_batch_exporter(exporter)
.build()
}
pub fn init_tracing_subscriber() -> OtelGuard {
let tracer_provider = init_tracer_provider();
let meter_provider = init_meter_provider();
let tracer = tracer_provider.tracer("ncb-tts-r2");
tracing_subscriber::registry()
.with(tracing_subscriber::filter::LevelFilter::from_level(
Level::INFO,
))
.with(tracing_subscriber::fmt::layer())
.with(MetricsLayer::new(meter_provider.clone()))
.with(OpenTelemetryLayer::new(tracer))
.init();
OtelGuard {
_tracer_provider: tracer_provider,
_meter_provider: meter_provider,
}
}
pub struct OtelGuard {
_tracer_provider: SdkTracerProvider,
_meter_provider: SdkMeterProvider,
}

View File

@ -1,17 +1,18 @@
use tokio::sync::RwLock;
use std::sync::Arc;
use crate::tts::gcp_tts::structs::{ use crate::tts::gcp_tts::structs::{
synthesize_request::SynthesizeRequest, synthesize_response::SynthesizeResponse, synthesize_request::SynthesizeRequest, synthesize_response::SynthesizeResponse,
}; };
use gcp_auth::Token; use gcp_auth::Token;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone)] #[derive(Clone, Debug)]
pub struct GCPTTS { pub struct GCPTTS {
pub token: Arc<RwLock<Token>>, pub token: Arc<RwLock<Token>>,
pub credentials_path: String, pub credentials_path: String,
} }
impl GCPTTS { impl GCPTTS {
#[tracing::instrument]
pub async fn update_token(&self) -> Result<(), gcp_auth::Error> { pub async fn update_token(&self) -> Result<(), gcp_auth::Error> {
let mut token = self.token.write().await; let mut token = self.token.write().await;
if token.has_expired() { if token.has_expired() {
@ -26,6 +27,7 @@ impl GCPTTS {
Ok(()) Ok(())
} }
#[tracing::instrument]
pub async fn new(credentials_path: String) -> Result<Self, gcp_auth::Error> { pub async fn new(credentials_path: String) -> Result<Self, gcp_auth::Error> {
let authenticator = gcp_auth::from_credentials_file(credentials_path.clone()).await?; let authenticator = gcp_auth::from_credentials_file(credentials_path.clone()).await?;
let token = authenticator let token = authenticator
@ -59,18 +61,19 @@ impl GCPTTS {
/// } /// }
/// }).await.unwrap(); /// }).await.unwrap();
/// ``` /// ```
#[tracing::instrument]
pub async fn synthesize( pub async fn synthesize(
&self, &self,
request: SynthesizeRequest, request: SynthesizeRequest,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> { ) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
self.update_token().await.unwrap(); self.update_token().await.unwrap();
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let token_string = { let token_string = {
let token = self.token.read().await; let token = self.token.read().await;
token.as_str().to_string() token.as_str().to_string()
}; };
match client match client
.post("https://texttospeech.googleapis.com/v1/text:synthesize") .post("https://texttospeech.googleapis.com/v1/text:synthesize")
.header(reqwest::header::CONTENT_TYPE, "application/json") .header(reqwest::header::CONTENT_TYPE, "application/json")

View File

@ -1,3 +1,5 @@
use std::fmt::Debug;
use serenity::{ use serenity::{
model::{ model::{
channel::Message, channel::Message,
@ -8,6 +10,7 @@ use serenity::{
use crate::tts::message::TTSMessage; use crate::tts::message::TTSMessage;
#[derive(Debug, Clone)]
pub struct TTSInstance { pub struct TTSInstance {
pub before_message: Option<Message>, pub before_message: Option<Message>,
pub text_channel: ChannelId, pub text_channel: ChannelId,
@ -22,9 +25,10 @@ impl TTSInstance {
/// ```rust /// ```rust
/// instance.read(message, &ctx).await; /// instance.read(message, &ctx).await;
/// ``` /// ```
#[tracing::instrument]
pub async fn read<T>(&mut self, message: T, ctx: &Context) pub async fn read<T>(&mut self, message: T, ctx: &Context)
where where
T: TTSMessage, T: TTSMessage + Debug,
{ {
let audio = message.synthesize(self, ctx).await; let audio = message.synthesize(self, ctx).await;
@ -38,6 +42,7 @@ impl TTSInstance {
} }
} }
#[tracing::instrument]
pub async fn skip(&mut self, ctx: &Context) { pub async fn skip(&mut self, ctx: &Context) {
let manager = songbird::get(&ctx).await.unwrap(); let manager = songbird::get(&ctx).await.unwrap();
let call = manager.get(self.guild).unwrap(); let call = manager.get(self.guild).unwrap();

View File

@ -29,6 +29,7 @@ pub trait TTSMessage {
async fn synthesize(&self, instance: &mut TTSInstance, ctx: &Context) -> Vec<Compressed>; async fn synthesize(&self, instance: &mut TTSInstance, ctx: &Context) -> Vec<Compressed>;
} }
#[derive(Debug, Clone)]
pub struct AnnounceMessage { pub struct AnnounceMessage {
pub message: String, pub message: String,
} }

View File

@ -1,6 +1,6 @@
pub mod gcp_tts; pub mod gcp_tts;
pub mod instance; pub mod instance;
pub mod message; pub mod message;
pub mod tts;
pub mod tts_type; pub mod tts_type;
pub mod voicevox; pub mod voicevox;
pub mod tts;

View File

@ -5,8 +5,18 @@ use lru::LruCache;
use songbird::{driver::Bitrate, input::cached::Compressed}; use songbird::{driver::Bitrate, input::cached::Compressed};
use tracing::info; use tracing::info;
use super::{gcp_tts::{gcp_tts::GCPTTS, structs::{synthesis_input::SynthesisInput, synthesize_request::SynthesizeRequest, voice_selection_params::VoiceSelectionParams}}, voicevox::voicevox::VOICEVOX}; use super::{
gcp_tts::{
gcp_tts::GCPTTS,
structs::{
synthesis_input::SynthesisInput, synthesize_request::SynthesizeRequest,
voice_selection_params::VoiceSelectionParams,
},
},
voicevox::voicevox::VOICEVOX,
};
#[derive(Debug)]
pub struct TTS { pub struct TTS {
pub voicevox_client: VOICEVOX, pub voicevox_client: VOICEVOX,
gcp_tts_client: GCPTTS, gcp_tts_client: GCPTTS,
@ -20,10 +30,7 @@ pub enum CacheKey {
} }
impl TTS { impl TTS {
pub fn new( pub fn new(voicevox_client: VOICEVOX, gcp_tts_client: GCPTTS) -> Self {
voicevox_client: VOICEVOX,
gcp_tts_client: GCPTTS,
) -> Self {
Self { Self {
voicevox_client, voicevox_client,
gcp_tts_client, gcp_tts_client,
@ -31,7 +38,12 @@ impl TTS {
} }
} }
pub async fn synthesize_voicevox(&self, text: &str, speaker: i64) -> Result<Compressed, Box<dyn std::error::Error>> { #[tracing::instrument]
pub async fn synthesize_voicevox(
&self,
text: &str,
speaker: i64,
) -> Result<Compressed, Box<dyn std::error::Error>> {
let cache_key = CacheKey::Voicevox(text.to_string(), speaker); let cache_key = CacheKey::Voicevox(text.to_string(), speaker);
let cached_audio = { let cached_audio = {
@ -43,15 +55,16 @@ impl TTS {
info!("Cache hit for VOICEVOX TTS"); info!("Cache hit for VOICEVOX TTS");
return Ok(audio); return Ok(audio);
} }
info!("Cache miss for VOICEVOX TTS"); info!("Cache miss for VOICEVOX TTS");
let audio = self.voicevox_client let audio = self
.voicevox_client
.synthesize(text.to_string(), speaker) .synthesize(text.to_string(), speaker)
.await?; .await?;
let compressed = Compressed::new(audio.into(), Bitrate::Auto).await?; let compressed = Compressed::new(audio.into(), Bitrate::Auto).await?;
{ {
let mut cache_guard = self.cache.write().unwrap(); let mut cache_guard = self.cache.write().unwrap();
cache_guard.put(cache_key, compressed.clone()); cache_guard.put(cache_key, compressed.clone());
@ -60,7 +73,11 @@ impl TTS {
Ok(compressed) Ok(compressed)
} }
pub async fn synthesize_gcp(&self, synthesize_request: SynthesizeRequest) -> Result<Compressed, Box<dyn std::error::Error>> { #[tracing::instrument]
pub async fn synthesize_gcp(
&self,
synthesize_request: SynthesizeRequest,
) -> Result<Compressed, Box<dyn std::error::Error>> {
let cache_key = CacheKey::GCP( let cache_key = CacheKey::GCP(
synthesize_request.input.clone(), synthesize_request.input.clone(),
synthesize_request.voice.clone(), synthesize_request.voice.clone(),
@ -75,15 +92,13 @@ impl TTS {
info!("Cache hit for GCP TTS"); info!("Cache hit for GCP TTS");
return Ok(audio); return Ok(audio);
} }
info!("Cache miss for GCP TTS"); info!("Cache miss for GCP TTS");
let audio = self.gcp_tts_client let audio = self.gcp_tts_client.synthesize(synthesize_request).await?;
.synthesize(synthesize_request)
.await?;
let compressed = Compressed::new(audio.into(), Bitrate::Auto).await?; let compressed = Compressed::new(audio.into(), Bitrate::Auto).await?;
{ {
let mut cache_guard = self.cache.write().unwrap(); let mut cache_guard = self.cache.write().unwrap();
cache_guard.put(cache_key, compressed.clone()); cache_guard.put(cache_key, compressed.clone());

View File

@ -2,12 +2,13 @@ use super::structs::speaker::Speaker;
const BASE_API_URL: &str = "https://deprecatedapis.tts.quest/v2/"; const BASE_API_URL: &str = "https://deprecatedapis.tts.quest/v2/";
#[derive(Clone)] #[derive(Clone, Debug)]
pub struct VOICEVOX { pub struct VOICEVOX {
pub key: String, pub key: String,
} }
impl VOICEVOX { impl VOICEVOX {
#[tracing::instrument]
pub async fn get_styles(&self) -> Vec<(String, i64)> { pub async fn get_styles(&self) -> Vec<(String, i64)> {
let speakers = self.get_speaker_list().await; let speakers = self.get_speaker_list().await;
let mut speaker_list = vec![]; let mut speaker_list = vec![];
@ -20,6 +21,7 @@ impl VOICEVOX {
speaker_list speaker_list
} }
#[tracing::instrument]
pub async fn get_speakers(&self) -> Vec<String> { pub async fn get_speakers(&self) -> Vec<String> {
let speakers = self.get_speaker_list().await; let speakers = self.get_speaker_list().await;
let mut speaker_list = vec![]; let mut speaker_list = vec![];
@ -34,6 +36,7 @@ impl VOICEVOX {
Self { key } Self { key }
} }
#[tracing::instrument]
async fn get_speaker_list(&self) -> Vec<Speaker> { async fn get_speaker_list(&self) -> Vec<Speaker> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
match client match client
@ -49,6 +52,7 @@ impl VOICEVOX {
} }
} }
#[tracing::instrument]
pub async fn synthesize( pub async fn synthesize(
&self, &self,
text: String, text: String,