diff --git a/Cargo.lock b/Cargo.lock index 0d8dd1581..46435d97d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1213,6 +1213,9 @@ dependencies = [ "nix 0.29.0", "objc", "objc2-app-kit", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", "png 0.17.16", "rand 0.8.5", "relative-path", @@ -1257,6 +1260,8 @@ dependencies = [ "tokio-util", "tracing", "tracing-appender", + "tracing-futures", + "tracing-opentelemetry", "tracing-subscriber", "uuid", "wgpu", @@ -6039,6 +6044,80 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.16", + "tracing", +] + +[[package]] +name = "opentelemetry-http" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" +dependencies = [ + "async-trait", + "bytes", + "http 1.3.1", + "opentelemetry", + "reqwest", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" +dependencies = [ + "http 1.3.1", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "reqwest", + "thiserror 2.0.16", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic", + "tonic-prost", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "rand 0.9.2", + "thiserror 2.0.16", + "tokio", + "tokio-stream", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -6367,6 +6446,26 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -6651,6 +6750,29 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "prost" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +dependencies = [ + "anyhow", + "itertools 0.13.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "psl-types" version = "2.0.11" @@ -9721,6 +9843,38 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" +[[package]] +name = "tonic" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "http 1.3.1", + "http-body", + "http-body-util", + "percent-encoding", + "pin-project", + "sync_wrapper", + "tokio-stream", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes", + "prost", + "tonic", +] + [[package]] name = "tower" version = "0.5.2" @@ -9812,6 +9966,18 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "futures", + "futures-task", + "pin-project", + "tracing", +] + [[package]] name = "tracing-log" version = "0.2.0" @@ -9823,6 +9989,25 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-opentelemetry" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6e5658463dd88089aba75c7791e1d3120633b1bfde22478b28f625a9bb1b8e" +dependencies = [ + "js-sys", + "opentelemetry", + "opentelemetry_sdk", + "rustversion", + "smallvec", + "thiserror 2.0.16", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + [[package]] name = "tracing-subscriber" version = "0.3.20" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 7f9c67d7b..c5f149fd7 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -107,6 +107,12 @@ tauri-plugin-sentry = "0.5.0" thiserror.workspace = true bytes = "1.10.1" async-stream = "0.3.6" +tracing-futures = { version = "0.2.5", features = ["futures-03"] } +tracing-opentelemetry = "0.32.0" +opentelemetry = "0.31.0" +opentelemetry-otlp = "0.31.0" #{ version = , features = ["http-proto", "reqwest-client"] } +opentelemetry_sdk = { version = "0.31.0", features = ["rt-tokio", "trace"] } + [target.'cfg(target_os = "macos")'.dependencies] core-graphics = "0.24.0" diff --git a/apps/desktop/src-tauri/src/api.rs b/apps/desktop/src-tauri/src/api.rs index fdfab9c4c..d60dbd949 100644 --- a/apps/desktop/src-tauri/src/api.rs +++ b/apps/desktop/src-tauri/src/api.rs @@ -4,9 +4,11 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use tauri::AppHandle; +use tracing::instrument; use crate::web_api::{AuthedApiError, ManagerExt}; +#[instrument] pub async fn upload_multipart_initiate( app: &AppHandle, video_id: &str, @@ -44,6 +46,7 @@ pub async fn upload_multipart_initiate( .map(|data| data.upload_id) } +#[instrument] pub async fn upload_multipart_presign_part( app: &AppHandle, video_id: &str, @@ -86,7 +89,7 @@ pub async fn upload_multipart_presign_part( .map(|data| data.presigned_url) } -#[derive(Serialize)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct UploadedPart { pub part_number: u32, @@ -107,6 +110,7 @@ pub struct S3VideoMeta { pub fps: Option, } +#[instrument] pub async fn upload_multipart_complete( app: &AppHandle, video_id: &str, @@ -158,7 +162,7 @@ pub async fn upload_multipart_complete( .map(|data| data.location) } -#[derive(Serialize)] +#[derive(Debug, Serialize)] #[serde(rename_all = "lowercase")] pub enum PresignedS3PutRequestMethod { #[allow(unused)] @@ -166,7 +170,7 @@ pub enum PresignedS3PutRequestMethod { Put, } -#[derive(Serialize)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct PresignedS3PutRequest { pub video_id: String, @@ -176,6 +180,7 @@ pub struct PresignedS3PutRequest { pub meta: Option, } +#[instrument(skip())] pub async fn upload_signed( app: &AppHandle, body: PresignedS3PutRequest, @@ -213,6 +218,7 @@ pub async fn upload_signed( .map(|data| data.presigned_put_data.url) } +#[instrument] pub async fn desktop_video_progress( app: &AppHandle, video_id: &str, diff --git a/apps/desktop/src-tauri/src/captions.rs b/apps/desktop/src-tauri/src/captions.rs index b00d3a21b..88e4ec457 100644 --- a/apps/desktop/src-tauri/src/captions.rs +++ b/apps/desktop/src-tauri/src/captions.rs @@ -16,6 +16,7 @@ use tauri::{AppHandle, Emitter, Manager, Window}; use tempfile::tempdir; use tokio::io::AsyncWriteExt; use tokio::sync::Mutex; +use tracing::instrument; use whisper_rs::{FullParams, SamplingStrategy, WhisperContext, WhisperContextParameters}; // Re-export caption types from cap_project @@ -48,6 +49,7 @@ const WHISPER_SAMPLE_RATE: u32 = 16000; /// Function to handle creating directories for the model #[tauri::command] #[specta::specta] +#[instrument] pub async fn create_dir(path: String, _recursive: bool) -> Result<(), String> { std::fs::create_dir_all(path).map_err(|e| format!("Failed to create directory: {e}")) } @@ -55,6 +57,7 @@ pub async fn create_dir(path: String, _recursive: bool) -> Result<(), String> { /// Function to save the model file #[tauri::command] #[specta::specta] +#[instrument] pub async fn save_model_file(path: String, data: Vec) -> Result<(), String> { std::fs::write(&path, &data).map_err(|e| format!("Failed to write model file: {e}")) } @@ -650,6 +653,7 @@ fn process_with_whisper( /// Function to transcribe audio from a video file using Whisper #[tauri::command] #[specta::specta] +#[instrument] pub async fn transcribe_audio( video_path: String, model_path: String, @@ -712,10 +716,11 @@ pub async fn transcribe_audio( /// Function to save caption data to a file #[tauri::command] #[specta::specta] +#[instrument(skip(app))] pub async fn save_captions( + app: AppHandle, video_id: String, captions: CaptionData, - app: AppHandle, ) -> Result<(), String> { tracing::info!("Saving captions for video_id: {}", video_id); @@ -966,9 +971,10 @@ pub fn parse_captions_json(json: &str) -> Result Result, String> { let captions_dir = app_captions_dir(&app, &video_id)?; let captions_path = captions_dir.join("captions.json"); @@ -1043,6 +1049,7 @@ impl DownloadProgress { /// Helper function to download a Whisper model from Hugging Face Hub #[tauri::command] #[specta::specta] +#[instrument(skip(window))] pub async fn download_whisper_model( window: Window, model_name: String, @@ -1134,6 +1141,7 @@ pub async fn download_whisper_model( /// Function to check if a model file exists #[tauri::command] #[specta::specta] +#[instrument] pub async fn check_model_exists(model_path: String) -> Result { Ok(std::path::Path::new(&model_path).exists()) } @@ -1141,6 +1149,7 @@ pub async fn check_model_exists(model_path: String) -> Result { /// Function to delete a downloaded model #[tauri::command] #[specta::specta] +#[instrument] pub async fn delete_whisper_model(model_path: String) -> Result<(), String> { if !std::path::Path::new(&model_path).exists() { return Err(format!("Model file not found: {model_path}")); @@ -1185,14 +1194,15 @@ fn format_srt_time(seconds: f64) -> String { /// Export captions to an SRT file #[tauri::command] #[specta::specta] +#[instrument(skip(app))] pub async fn export_captions_srt( - video_id: String, app: AppHandle, + video_id: String, ) -> Result, String> { tracing::info!("Starting SRT export for video_id: {}", video_id); // Load captions - let captions = match load_captions(video_id.clone(), app.clone()).await? { + let captions = match load_captions(app.clone(), video_id.clone()).await? { Some(c) => { tracing::info!("Found {} caption segments to export", c.segments.len()); c diff --git a/apps/desktop/src-tauri/src/export.rs b/apps/desktop/src-tauri/src/export.rs index 2170bacdf..3196d7bed 100644 --- a/apps/desktop/src-tauri/src/export.rs +++ b/apps/desktop/src-tauri/src/export.rs @@ -4,7 +4,7 @@ use cap_project::{RecordingMeta, XY}; use serde::Deserialize; use specta::Type; use std::path::PathBuf; -use tracing::info; +use tracing::{info, instrument}; #[derive(Deserialize, Clone, Copy, Debug, Type)] #[serde(tag = "format")] @@ -24,6 +24,7 @@ impl ExportSettings { #[tauri::command] #[specta::specta] +#[instrument(skip(progress))] pub async fn export_video( project_path: PathBuf, progress: tauri::ipc::Channel, @@ -88,6 +89,7 @@ pub struct ExportEstimates { // This will need to be refactored at some point to be more accurate. #[tauri::command] #[specta::specta] +#[instrument] pub async fn get_export_estimates( path: PathBuf, resolution: XY, diff --git a/apps/desktop/src-tauri/src/fake_window.rs b/apps/desktop/src-tauri/src/fake_window.rs index b474b5726..149b0952e 100644 --- a/apps/desktop/src-tauri/src/fake_window.rs +++ b/apps/desktop/src-tauri/src/fake_window.rs @@ -2,11 +2,13 @@ use scap_targets::bounds::LogicalBounds; use std::{collections::HashMap, sync::Arc, time::Duration}; use tauri::{AppHandle, Manager, WebviewWindow}; use tokio::{sync::RwLock, time::sleep}; +use tracing::instrument; pub struct FakeWindowBounds(pub Arc>>>); #[tauri::command] #[specta::specta] +#[instrument(skip(state))] pub async fn set_fake_window_bounds( window: tauri::Window, name: String, @@ -23,6 +25,7 @@ pub async fn set_fake_window_bounds( #[tauri::command] #[specta::specta] +#[instrument(skip(state, window))] pub async fn remove_fake_window( window: tauri::Window, name: String, diff --git a/apps/desktop/src-tauri/src/general_settings.rs b/apps/desktop/src-tauri/src/general_settings.rs index ada685f66..eba4cf1d3 100644 --- a/apps/desktop/src-tauri/src/general_settings.rs +++ b/apps/desktop/src-tauri/src/general_settings.rs @@ -4,7 +4,7 @@ use serde_json::json; use specta::Type; use tauri::{AppHandle, Wry}; use tauri_plugin_store::StoreExt; -use tracing::error; +use tracing::{error, instrument}; use uuid::Uuid; #[derive(Default, Serialize, Deserialize, Type, Debug, Clone, Copy)] @@ -256,6 +256,7 @@ pub fn init(app: &AppHandle) { #[tauri::command] #[specta::specta] +#[instrument] pub fn get_default_excluded_windows() -> Vec { default_excluded_windows() } diff --git a/apps/desktop/src-tauri/src/hotkeys.rs b/apps/desktop/src-tauri/src/hotkeys.rs index 79a4a5eff..14bf3d38c 100644 --- a/apps/desktop/src-tauri/src/hotkeys.rs +++ b/apps/desktop/src-tauri/src/hotkeys.rs @@ -11,8 +11,9 @@ use tauri::{AppHandle, Manager}; use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut}; use tauri_plugin_store::StoreExt; use tauri_specta::Event; +use tracing::instrument; -#[derive(Serialize, Deserialize, Type, PartialEq, Clone, Copy)] +#[derive(Serialize, Deserialize, Type, PartialEq, Clone, Copy, Debug)] pub struct Hotkey { #[specta(type = String)] code: Code, @@ -180,6 +181,7 @@ async fn handle_hotkey(app: AppHandle, action: HotkeyAction) -> Result<(), Strin #[tauri::command(async)] #[specta::specta] +#[instrument(skip(app))] pub fn set_hotkey(app: AppHandle, action: HotkeyAction, hotkey: Option) -> Result<(), ()> { let global_shortcut = app.global_shortcut(); let state = app.state::(); diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 05289bd2d..29338b56c 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -66,6 +66,7 @@ use serde_json::json; use specta::Type; use std::{ collections::BTreeMap, + fmt, fs::File, future::Future, io::BufWriter, @@ -86,7 +87,7 @@ use tauri_specta::Event; #[cfg(target_os = "macos")] use tokio::sync::Mutex; use tokio::sync::{RwLock, oneshot}; -use tracing::{error, trace, warn}; +use tracing::{error, instrument, trace, warn}; use upload::{create_or_get_video, upload_image, upload_video}; use web_api::AuthedApiError; use web_api::ManagerExt as WebManagerExt; @@ -220,6 +221,7 @@ impl App { #[tauri::command] #[specta::specta] +#[instrument(skip(state))] async fn set_mic_input(state: MutableState<'_, App>, label: Option) -> Result<(), String> { let mic_feed = state.read().await.mic_feed.clone(); @@ -245,6 +247,7 @@ async fn set_mic_input(state: MutableState<'_, App>, label: Option) -> R #[tauri::command] #[specta::specta] +#[instrument(skip(app_handle, state))] async fn set_camera_input( app_handle: AppHandle, state: MutableState<'_, App>, @@ -388,6 +391,7 @@ struct CurrentRecording { #[tauri::command] #[specta::specta] +#[instrument(skip(state))] async fn get_current_recording( state: MutableState<'_, App>, ) -> Result>, ()> { @@ -575,6 +579,7 @@ async fn create_screenshot( #[tauri::command] #[specta::specta] +#[instrument(skip(app))] async fn copy_file_to_path(app: AppHandle, src: String, dst: String) -> Result<(), String> { println!("Attempting to copy file from {src} to {dst}"); @@ -698,6 +703,7 @@ pub fn is_valid_video(path: &std::path::Path) -> bool { #[tauri::command] #[specta::specta] +#[instrument(skip(clipboard))] async fn copy_screenshot_to_clipboard( clipboard: MutableState<'_, ClipboardContext>, path: String, @@ -716,6 +722,7 @@ async fn copy_screenshot_to_clipboard( #[tauri::command] #[specta::specta] +#[instrument(skip(_app))] async fn open_file_path(_app: AppHandle, path: PathBuf) -> Result<(), String> { let path_str = path.to_str().ok_or("Invalid path")?; @@ -774,6 +781,7 @@ impl EditorStateChanged { #[tauri::command] #[specta::specta] +#[instrument(skip(editor_instance))] async fn start_playback( editor_instance: WindowEditorInstance, fps: u32, @@ -786,6 +794,7 @@ async fn start_playback( #[tauri::command] #[specta::specta] +#[instrument(skip(editor_instance))] async fn stop_playback(editor_instance: WindowEditorInstance) -> Result<(), String> { let mut state = editor_instance.state.lock().await; @@ -808,6 +817,7 @@ struct SerializedEditorInstance { #[tauri::command] #[specta::specta] +#[instrument(skip(window))] async fn create_editor_instance(window: Window) -> Result { let CapWindowId::Editor { id } = CapWindowId::from_str(window.label()).unwrap() else { return Err("Invalid window".to_string()); @@ -843,12 +853,14 @@ async fn create_editor_instance(window: Window) -> Result Result { let path = editor.project_path.clone(); RecordingMeta::load_for_project(&path).map_err(|e| e.to_string()) } #[tauri::command] #[specta::specta] +#[instrument(skip(editor))] async fn set_pretty_name(editor: WindowEditorInstance, pretty_name: String) -> Result<(), String> { let mut meta = editor.meta().clone(); meta.pretty_name = pretty_name; @@ -857,6 +869,7 @@ async fn set_pretty_name(editor: WindowEditorInstance, pretty_name: String) -> R #[tauri::command] #[specta::specta] +#[instrument(skip(app, clipboard))] async fn copy_video_to_clipboard( app: AppHandle, clipboard: MutableState<'_, ClipboardContext>, @@ -874,6 +887,7 @@ async fn copy_video_to_clipboard( #[tauri::command] #[specta::specta] +#[instrument] async fn get_video_metadata(path: PathBuf) -> Result { let recording_meta = RecordingMeta::load_for_project(&path).map_err(|v| v.to_string())?; @@ -953,6 +967,7 @@ async fn get_video_metadata(path: PathBuf) -> Result Result, String> { @@ -1032,6 +1051,7 @@ async fn generate_zoom_segments_from_clicks( #[tauri::command] #[specta::specta] +#[instrument] async fn list_audio_devices() -> Result, ()> { if !permissions::do_permissions_check(false) .microphone @@ -1048,7 +1068,7 @@ pub struct UploadProgress { progress: f64, } -#[derive(Deserialize, Type)] +#[derive(Debug, Deserialize, Type)] pub enum UploadMode { Initial { pre_created_video: Option, @@ -1058,6 +1078,7 @@ pub enum UploadMode { #[tauri::command] #[specta::specta] +#[instrument(skip(app, channel))] async fn upload_exported_video( app: AppHandle, path: PathBuf, @@ -1182,6 +1203,7 @@ async fn upload_exported_video( #[tauri::command] #[specta::specta] +#[instrument(skip(app, clipboard))] async fn upload_screenshot( app: AppHandle, clipboard: MutableState<'_, ClipboardContext>, @@ -1231,6 +1253,7 @@ async fn upload_screenshot( #[tauri::command] #[specta::specta] +#[instrument(skip(app, _state))] async fn take_screenshot(app: AppHandle, _state: MutableState<'_, App>) -> Result<(), String> { let id = uuid::Uuid::new_v4().to_string(); @@ -1359,6 +1382,7 @@ async fn take_screenshot(app: AppHandle, _state: MutableState<'_, App>) -> Resul #[tauri::command] #[specta::specta] +#[instrument(skip(app))] async fn save_file_dialog( app: AppHandle, file_name: String, @@ -1478,6 +1502,7 @@ pub enum FileType { #[tauri::command(async)] #[specta::specta] +#[instrument] fn get_recording_meta( path: PathBuf, _file_type: FileType, @@ -1489,6 +1514,7 @@ fn get_recording_meta( #[tauri::command] #[specta::specta] +#[instrument(skip(app))] fn list_recordings(app: AppHandle) -> Result, String> { let recordings_dir = recordings_path(&app); @@ -1535,6 +1561,7 @@ fn list_recordings(app: AppHandle) -> Result Result, String> { let screenshots_dir = screenshots_path(&app); @@ -1578,6 +1605,7 @@ fn list_screenshots(app: AppHandle) -> Result, Str #[tauri::command] #[specta::specta] +#[instrument(skip(app))] async fn check_upgraded_and_update(app: AppHandle) -> Result { println!("Checking upgraded status and updating..."); @@ -1640,6 +1668,7 @@ async fn check_upgraded_and_update(app: AppHandle) -> Result { #[tauri::command] #[specta::specta] +#[instrument(skip(app))] fn open_external_link(app: tauri::AppHandle, url: String) -> Result<(), String> { if let Ok(Some(settings)) = GeneralSettingsStore::get(&app) && settings.disable_auto_open_links @@ -1655,6 +1684,7 @@ fn open_external_link(app: tauri::AppHandle, url: String) -> Result<(), String> #[tauri::command] #[specta::specta] +#[instrument(skip(_app))] async fn reset_camera_permissions(_app: AppHandle) -> Result<(), String> { #[cfg(target_os = "macos")] { @@ -1677,6 +1707,7 @@ async fn reset_camera_permissions(_app: AppHandle) -> Result<(), String> { #[tauri::command] #[specta::specta] +#[instrument(skip(_app))] async fn reset_microphone_permissions(_app: AppHandle) -> Result<(), ()> { #[cfg(debug_assertions)] let bundle_id = "com.apple.Terminal"; @@ -1695,12 +1726,14 @@ async fn reset_microphone_permissions(_app: AppHandle) -> Result<(), ()> { #[tauri::command] #[specta::specta] +#[instrument(skip(app))] async fn is_camera_window_open(app: AppHandle) -> bool { CapWindowId::Camera.get(&app).is_some() } #[tauri::command] #[specta::specta] +#[instrument(skip(editor_instance))] async fn seek_to(editor_instance: WindowEditorInstance, frame_number: u32) -> Result<(), String> { editor_instance .modify_and_emit_state(|state| { @@ -1713,6 +1746,7 @@ async fn seek_to(editor_instance: WindowEditorInstance, frame_number: u32) -> Re #[tauri::command] #[specta::specta] +#[instrument(skip(editor_instance))] async fn get_mic_waveforms(editor_instance: WindowEditorInstance) -> Result>, String> { let mut out = Vec::new(); @@ -1729,6 +1763,7 @@ async fn get_mic_waveforms(editor_instance: WindowEditorInstance) -> Result Result>, String> { @@ -1747,6 +1782,7 @@ async fn get_system_audio_waveforms( #[tauri::command] #[specta::specta] +#[instrument(skip(app, editor_instance, window))] async fn editor_delete_project( app: tauri::AppHandle, editor_instance: WindowEditorInstance, @@ -1769,6 +1805,7 @@ async fn editor_delete_project( // keep this async otherwise opening windows may hang on windows #[tauri::command] #[specta::specta] +#[instrument(skip(app))] async fn show_window(app: AppHandle, window: ShowCapWindow) -> Result<(), String> { let _ = window.show(&app).await; Ok(()) @@ -1776,12 +1813,14 @@ async fn show_window(app: AppHandle, window: ShowCapWindow) -> Result<(), String #[tauri::command(async)] #[specta::specta] +#[instrument] fn list_fails() -> Result, ()> { Ok(cap_fail::get_state()) } #[tauri::command(async)] #[specta::specta] +#[instrument] fn set_fail(name: String, value: bool) { cap_fail::set_fail(&name, value) } @@ -1846,6 +1885,7 @@ async fn check_notification_permissions(app: AppHandle) { #[tauri::command] #[specta::specta] +#[instrument(skip(app))] async fn set_server_url(app: MutableState<'_, App>, server_url: String) -> Result<(), ()> { app.write().await.server_url = server_url; Ok(()) @@ -1853,6 +1893,7 @@ async fn set_server_url(app: MutableState<'_, App>, server_url: String) -> Resul #[tauri::command] #[specta::specta] +#[instrument(skip(app))] async fn set_camera_preview_state( app: MutableState<'_, App>, state: CameraPreviewState, @@ -1868,6 +1909,7 @@ async fn set_camera_preview_state( #[tauri::command] #[specta::specta] +#[instrument(skip(app))] async fn await_camera_preview_ready(app: MutableState<'_, App>) -> Result { let app = app.read().await.camera_feed.clone(); @@ -1882,11 +1924,12 @@ async fn await_camera_preview_ready(app: MutableState<'_, App>) -> Result bool>, tracing_subscriber::Registry, >; @@ -2647,12 +2690,14 @@ fn screenshots_path(app: &AppHandle) -> PathBuf { #[tauri::command] #[specta::specta] +#[instrument(skip(app))] fn global_message_dialog(app: AppHandle, message: String) { app.dialog().message(message).show(|_| {}); } #[tauri::command] #[specta::specta] +#[instrument(skip(clipboard))] async fn write_clipboard_string( clipboard: MutableState<'_, ClipboardContext>, text: String, diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 9dcaab06d..097261ede 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -47,7 +47,7 @@ fn main() { (sentry_client, _guard) }); - let (layer, handle) = tracing_subscriber::reload::Layer::new(None::); + let (reload_layer, handle) = tracing_subscriber::reload::Layer::new(None::); let logs_dir = { #[cfg(target_os = "macos")] @@ -73,12 +73,42 @@ fn main() { let file_appender = tracing_appender::rolling::daily(&logs_dir, "cap-desktop.log"); let (non_blocking, _logger_guard) = tracing_appender::non_blocking(file_appender); - let registry = tracing_subscriber::registry().with(tracing_subscriber::filter::filter_fn( - (|v| v.target().starts_with("cap_")) as fn(&tracing::Metadata) -> bool, - )); + let (otel_layer, _tracer) = if cfg!(debug_assertions) { + use opentelemetry::trace::TracerProvider; + use opentelemetry_otlp::WithExportConfig; + use tracing_subscriber::Layer; + + let tracer = opentelemetry_sdk::trace::SdkTracerProvider::builder() + .with_batch_exporter( + opentelemetry_otlp::SpanExporter::builder() + .with_http() + .with_protocol(opentelemetry_otlp::Protocol::HttpJson) + .build() + .unwrap(), + ) + .with_resource( + opentelemetry_sdk::Resource::builder() + .with_service_name("cap-desktop") + .build(), + ) + .build(); + + let layer = tracing_opentelemetry::layer() + .with_tracer(tracer.tracer("cap-desktop")) + .boxed(); + + opentelemetry::global::set_tracer_provider(tracer.clone()); + (Some(layer), Some(tracer)) + } else { + (None, None) + }; - registry - .with(layer) + tracing_subscriber::registry() + .with(tracing_subscriber::filter::filter_fn( + (|v| v.target().starts_with("cap_")) as fn(&tracing::Metadata) -> bool, + )) + .with(reload_layer) + .with(otel_layer) .with( tracing_subscriber::fmt::layer() .with_ansi(true) diff --git a/apps/desktop/src-tauri/src/permissions.rs b/apps/desktop/src-tauri/src/permissions.rs index 73dc886d7..32fd857f9 100644 --- a/apps/desktop/src-tauri/src/permissions.rs +++ b/apps/desktop/src-tauri/src/permissions.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; #[cfg(target_os = "macos")] use cidre::av; +use tracing::instrument; #[cfg(target_os = "macos")] #[link(name = "ApplicationServices", kind = "framework")] @@ -11,7 +12,7 @@ unsafe extern "C" { -> bool; } -#[derive(Serialize, Deserialize, specta::Type)] +#[derive(Debug, Serialize, Deserialize, specta::Type)] #[serde(rename_all = "camelCase")] pub enum OSPermission { ScreenRecording, @@ -61,6 +62,7 @@ pub fn open_permission_settings(_permission: OSPermission) { #[tauri::command] #[specta::specta] +#[instrument] pub async fn request_permission(_permission: OSPermission) { #[cfg(target_os = "macos")] { diff --git a/apps/desktop/src-tauri/src/platform/mod.rs b/apps/desktop/src-tauri/src/platform/mod.rs index 5a03c820c..267ec61b9 100644 --- a/apps/desktop/src-tauri/src/platform/mod.rs +++ b/apps/desktop/src-tauri/src/platform/mod.rs @@ -8,6 +8,7 @@ pub mod macos; #[cfg(target_os = "macos")] pub use macos::*; +use tracing::instrument; #[derive(Debug, Serialize, Deserialize, Type, Default)] #[repr(isize)] @@ -29,6 +30,7 @@ pub enum HapticPerformanceTime { #[tauri::command] #[specta::specta] +#[instrument] pub fn perform_haptic_feedback( _pattern: Option, _time: Option, diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index c6d212540..a9617babf 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -213,6 +213,7 @@ pub fn list_cameras() -> Vec { #[tauri::command] #[specta::specta] +#[instrument] pub async fn list_displays_with_thumbnails() -> Result, String> { tokio::task::spawn_blocking(|| { tauri::async_runtime::block_on(collect_displays_with_thumbnails()) @@ -223,6 +224,7 @@ pub async fn list_displays_with_thumbnails() -> Result Result, String> { tokio::task::spawn_blocking( || tauri::async_runtime::block_on(collect_windows_with_thumbnails()), @@ -663,6 +665,7 @@ pub async fn start_recording( #[tauri::command] #[specta::specta] +#[instrument(skip(state))] pub async fn pause_recording(state: MutableState<'_, App>) -> Result<(), String> { let mut state = state.write().await; @@ -675,6 +678,7 @@ pub async fn pause_recording(state: MutableState<'_, App>) -> Result<(), String> #[tauri::command] #[specta::specta] +#[instrument(skip(state))] pub async fn resume_recording(state: MutableState<'_, App>) -> Result<(), String> { let mut state = state.write().await; @@ -687,6 +691,7 @@ pub async fn resume_recording(state: MutableState<'_, App>) -> Result<(), String #[tauri::command] #[specta::specta] +#[instrument(skip(app, state))] pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> { let mut state = state.write().await; let Some(current_recording) = state.clear_current_recording() else { @@ -703,6 +708,7 @@ pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Res #[tauri::command] #[specta::specta] +#[instrument(skip(app, state))] pub async fn restart_recording( app: AppHandle, state: MutableState<'_, App>, @@ -724,6 +730,7 @@ pub async fn restart_recording( #[tauri::command] #[specta::specta] +#[instrument(skip(app, state))] pub async fn delete_recording(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> { let recording_data = { let mut app_state = state.write().await; diff --git a/apps/desktop/src-tauri/src/target_select_overlay.rs b/apps/desktop/src-tauri/src/target_select_overlay.rs index c218bc528..c9aab06b1 100644 --- a/apps/desktop/src-tauri/src/target_select_overlay.rs +++ b/apps/desktop/src-tauri/src/target_select_overlay.rs @@ -18,7 +18,7 @@ use tauri::{AppHandle, Manager, WebviewWindow}; use tauri_plugin_global_shortcut::{GlobalShortcut, GlobalShortcutExt}; use tauri_specta::Event; use tokio::task::JoinHandle; -use tracing::error; +use tracing::{error, instrument}; #[derive(tauri_specta::Event, Serialize, Type, Clone)] pub struct TargetUnderCursor { @@ -42,6 +42,7 @@ pub struct DisplayInformation { #[specta::specta] #[tauri::command] +#[instrument(skip(app, state))] pub async fn open_target_select_overlays( app: AppHandle, state: tauri::State<'_, WindowFocusManager>, @@ -102,6 +103,7 @@ pub async fn open_target_select_overlays( #[specta::specta] #[tauri::command] +#[instrument(skip(app))] pub async fn close_target_select_overlays(app: AppHandle) -> Result<(), String> { for (id, window) in app.webview_windows() { if let Ok(CapWindowId::TargetSelectOverlay { .. }) = CapWindowId::from_str(&id) { @@ -114,6 +116,7 @@ pub async fn close_target_select_overlays(app: AppHandle) -> Result<(), String> #[specta::specta] #[tauri::command] +#[instrument] pub async fn get_window_icon(window_id: &str) -> Result, String> { let window_id = window_id .parse::() @@ -127,6 +130,7 @@ pub async fn get_window_icon(window_id: &str) -> Result, String> #[specta::specta] #[tauri::command] +#[instrument] pub async fn display_information(display_id: &str) -> Result { let display_id = display_id .parse::() @@ -142,6 +146,7 @@ pub async fn display_information(display_id: &str) -> Result Result<(), String> { let window = Window::from_id(&window_id).ok_or("Window not found")?; diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index 556dfc2a7..f741806eb 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -17,11 +17,10 @@ use flume::Receiver; use futures::{Stream, StreamExt, TryStreamExt, stream}; use image::{ImageReader, codecs::jpeg::JpegEncoder}; use reqwest::StatusCode; -use sentry::types::Auth; use serde::{Deserialize, Serialize}; use specta::Type; use std::{ - collections::{HashMap, HashSet}, + collections::HashMap, io, path::{Path, PathBuf}, pin::pin, @@ -35,10 +34,11 @@ use tokio::{ fs::File, io::{AsyncReadExt, AsyncSeekExt, BufReader}, task::{self, JoinHandle}, - time, + time::{self, Instant}, }; use tokio_util::io::ReaderStream; -use tracing::{debug, error, info}; +use tracing::{Span, debug, error, info, instrument, trace}; +use tracing_futures::Instrument; pub struct UploadedItem { pub link: String, @@ -57,6 +57,7 @@ pub struct UploadProgressEvent { // a typical recommended chunk size is 5MB (AWS min part size). const CHUNK_SIZE: u64 = 5 * 1024 * 1024; // 5MB +#[instrument(skip(app, channel))] pub async fn upload_video( app: &AppHandle, video_id: String, @@ -169,6 +170,7 @@ async fn file_reader_stream(path: impl AsRef) -> Result<(ReaderStream Result { let input = ffmpeg::format::input(path).map_err(|e| format!("Failed to read input file: {e}"))?; @@ -301,6 +305,7 @@ pub fn build_video_meta(path: &PathBuf) -> Result { }) } +#[instrument] pub async fn compress_image(path: PathBuf) -> Result, String> { task::spawn_blocking(move || { let img = ImageReader::open(&path) @@ -456,6 +461,7 @@ pub struct Chunk { /// Creates a stream that reads chunks from a file, yielding [Chunk]'s. #[allow(unused)] +#[instrument] pub fn from_file_to_chunks(path: PathBuf) -> impl Stream> { try_stream! { let file = File::open(path).await?; @@ -475,11 +481,13 @@ pub fn from_file_to_chunks(path: PathBuf) -> impl Stream>, @@ -583,6 +591,7 @@ pub fn from_pending_file_to_chunks( } } } + .instrument(Span::current()) } fn retryable_client(host: String) -> reqwest::ClientBuilder { @@ -607,6 +616,7 @@ fn retryable_client(host: String) -> reqwest::ClientBuilder { /// Takes an incoming stream of bytes and individually uploads them to S3. /// /// Note: It's on the caller to ensure the chunks are sized correctly within S3 limits. +#[instrument(skip(app, stream))] fn multipart_uploader( app: AppHandle, video_id: String, @@ -614,13 +624,15 @@ fn multipart_uploader( stream: impl Stream>, ) -> impl Stream> { debug!("Initializing multipart uploader for video {video_id:?}"); + let start = Instant::now(); try_stream! { let mut stream = pin!(stream); let mut prev_part_number = None; + while let Some(item) = stream.next().await { let Chunk { total_size, part_number, chunk } = item.map_err(|err| format!("uploader/part/{:?}/fs: {err:?}", prev_part_number.map(|p| p + 1)))?; - debug!("Uploading chunk {part_number} ({} bytes) for video {video_id:?}", chunk.len()); + trace!("Uploading chunk {part_number} ({} bytes) for video {video_id:?}", chunk.len()); prev_part_number = Some(part_number); let md5_sum = base64::encode(md5::compute(&chunk).0); let size = chunk.len(); @@ -629,6 +641,8 @@ fn multipart_uploader( api::upload_multipart_presign_part(&app, &video_id, &upload_id, part_number, &md5_sum) .await?; + trace!("Uploading part {part_number}"); + let url = Uri::from_str(&presigned_url).map_err(|err| format!("uploader/part/{part_number}/invalid_url: {err:?}"))?; let resp = retryable_client(url.host().unwrap_or("").to_string()) .build() @@ -649,6 +663,8 @@ fn multipart_uploader( false => Ok(()), }?; + trace!("Completed upload of part {part_number}"); + yield UploadedPart { etag: etag.ok_or_else(|| format!("uploader/part/{part_number}/error: ETag header not found"))?, part_number, @@ -656,10 +672,14 @@ fn multipart_uploader( total_size }; } + + debug!("Completed multipart upload for {video_id:?} in {:?}", start.elapsed()); } + .instrument(Span::current()) } /// Takes an incoming stream of bytes and streams them to an S3 object. +#[instrument(skip(app, stream))] pub async fn singlepart_uploader( app: AppHandle, request: PresignedS3PutRequest, diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs index 5a5b11397..524541418 100644 --- a/apps/desktop/src-tauri/src/windows.rs +++ b/apps/desktop/src-tauri/src/windows.rs @@ -18,7 +18,7 @@ use tauri::{ }; use tauri_specta::Event; use tokio::sync::RwLock; -use tracing::{debug, error, warn}; +use tracing::{debug, error, instrument, warn}; use crate::{ App, ArcLock, RequestScreenCapturePrewarm, fake_window, @@ -178,7 +178,7 @@ impl CapWindowId { } } -#[derive(Clone, Type, Deserialize)] +#[derive(Debug, Clone, Type, Deserialize)] pub enum ShowCapWindow { Setup, Main { @@ -816,6 +816,7 @@ fn add_traffic_lights(window: &WebviewWindow, controls_inset: Option None, @@ -832,6 +833,7 @@ pub fn set_theme(window: tauri::Window, theme: AppTheme) { #[tauri::command] #[specta::specta] +#[instrument(skip(_window))] pub fn position_traffic_lights(_window: tauri::Window, _controls_inset: Option<(f64, f64)>) { #[cfg(target_os = "macos")] position_traffic_lights_impl( @@ -881,6 +883,7 @@ fn should_protect_window(app: &AppHandle, window_title: &str) -> bool { #[tauri::command] #[specta::specta] +#[instrument(skip(app))] pub fn refresh_window_content_protection(app: AppHandle) -> Result<(), String> { for (label, window) in app.webview_windows() { if let Ok(id) = CapWindowId::from_str(&label) { @@ -963,6 +966,7 @@ impl MonitorExt for Display { #[specta::specta] #[tauri::command(async)] +#[instrument(skip(_window))] pub fn set_window_transparent(_window: tauri::Window, _value: bool) { #[cfg(target_os = "macos")] { diff --git a/package.json b/package.json index 5f201da2f..40d8264d0 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "web": "pnpm --filter=@cap/web", "env-setup": "node scripts/env-cli.js", "check-tauri-versions": "node scripts/check-tauri-plugin-versions.js", - "clean": "find . -name node_modules -o -name .next -o -name .output -o -name .turbo -o -name dist -type d -prune | xargs rm -rf" + "clean": "find . -name node_modules -o -name .next -o -name .output -o -name .turbo -o -name dist -type d -prune | xargs rm -rf", + "lgtm-otel": "docker run -p 3010:3000 -p 4317:4317 -p 4318:4318 --rm -it docker.io/grafana/otel-lgtm" }, "devDependencies": { "@biomejs/biome": "2.2.0",