trace_db_call no longer takes an OpenTelemetry Context: it opens a db.<query_type>.<operation> span (same name via otel.name, same db.query_type / db.operation attrs) on the caller's ambient tracing span and runs the DAO body inside it. The cx / ctx / context parameter is gone from every DAO trait method, impl and call site, and from the helpers and background drains that only threaded it. The remaining request-handler spans (files, tags, faces, image, video, favorites, duplicates, memories) move to #[instrument]; the Context::current_with_span / with_remote_span_context plumbing is gone, as is extract_context_from_request (the tracing-actix-web root span covers it). otel::set_span_attributes replaces BoxedSpan::set_attributes. Background-job raw spans (thumbnails, video actors) are left for #142. tests/otel_db_spans.rs pins name, attributes, status and nesting (across an instrumented spawn); otel_traceparent now uses an #[instrument] handler. CLAUDE.md documents the tracing conventions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
4.5 KiB
Rust
170 lines
4.5 KiB
Rust
use actix_web::{
|
|
HttpResponse,
|
|
body::{BoxBody, MessageBody},
|
|
};
|
|
|
|
use crate::database::models::{User, VideoPreviewClip};
|
|
use crate::database::{DbError, DbErrorKind, PreviewDao, UserDao};
|
|
use std::cell::RefCell;
|
|
use std::collections::HashMap;
|
|
use std::option::Option;
|
|
use std::sync::Mutex as StdMutex;
|
|
|
|
pub struct TestUserDao {
|
|
pub user_map: RefCell<Vec<User>>,
|
|
}
|
|
|
|
impl Default for TestUserDao {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl TestUserDao {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
user_map: RefCell::new(Vec::new()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl UserDao for TestUserDao {
|
|
fn create_user(&mut self, username: &str, password: &str) -> Option<User> {
|
|
let u = User {
|
|
id: (self.user_map.borrow().len() + 1) as i32,
|
|
username: username.to_string(),
|
|
password: password.to_string(),
|
|
};
|
|
|
|
self.user_map.borrow_mut().push(u.clone());
|
|
|
|
Some(u)
|
|
}
|
|
|
|
fn get_user(&mut self, user: &str, pass: &str) -> Option<User> {
|
|
match self
|
|
.user_map
|
|
.borrow()
|
|
.iter()
|
|
.find(|&u| u.username == user && u.password == pass)
|
|
{
|
|
Some(u) => {
|
|
let copy = (*u).clone();
|
|
Some(copy)
|
|
}
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
fn user_exists(&mut self, user: &str) -> bool {
|
|
self.user_map.borrow().iter().any(|u| u.username == user)
|
|
}
|
|
}
|
|
|
|
pub trait BodyReader {
|
|
fn read_to_str(self) -> String;
|
|
}
|
|
|
|
impl BodyReader for HttpResponse<BoxBody> {
|
|
fn read_to_str(self) -> String {
|
|
let body = self.into_body().try_into_bytes().unwrap();
|
|
std::str::from_utf8(&body).unwrap().to_string()
|
|
}
|
|
}
|
|
|
|
pub struct TestPreviewDao {
|
|
pub clips: StdMutex<HashMap<String, VideoPreviewClip>>,
|
|
next_id: StdMutex<i32>,
|
|
}
|
|
|
|
impl Default for TestPreviewDao {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl TestPreviewDao {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
clips: StdMutex::new(HashMap::new()),
|
|
next_id: StdMutex::new(1),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PreviewDao for TestPreviewDao {
|
|
fn insert_preview(&mut self, file_path_val: &str, status_val: &str) -> Result<(), DbError> {
|
|
let mut clips = self.clips.lock().unwrap();
|
|
// insert_or_ignore semantics: skip if key already exists
|
|
if clips.contains_key(file_path_val) {
|
|
return Ok(());
|
|
}
|
|
let mut id = self.next_id.lock().unwrap();
|
|
let now = chrono::Utc::now().to_rfc3339();
|
|
clips.insert(
|
|
file_path_val.to_string(),
|
|
VideoPreviewClip {
|
|
id: *id,
|
|
library_id: crate::libraries::PRIMARY_LIBRARY_ID,
|
|
file_path: file_path_val.to_string(),
|
|
status: status_val.to_string(),
|
|
duration_seconds: None,
|
|
file_size_bytes: None,
|
|
error_message: None,
|
|
created_at: now.clone(),
|
|
updated_at: now,
|
|
},
|
|
);
|
|
*id += 1;
|
|
Ok(())
|
|
}
|
|
|
|
fn update_status(
|
|
&mut self,
|
|
file_path_val: &str,
|
|
status_val: &str,
|
|
duration: Option<f32>,
|
|
size: Option<i32>,
|
|
error: Option<&str>,
|
|
) -> Result<(), DbError> {
|
|
let mut clips = self.clips.lock().unwrap();
|
|
if let Some(clip) = clips.get_mut(file_path_val) {
|
|
clip.status = status_val.to_string();
|
|
clip.duration_seconds = duration;
|
|
clip.file_size_bytes = size;
|
|
clip.error_message = error.map(|s| s.to_string());
|
|
clip.updated_at = chrono::Utc::now().to_rfc3339();
|
|
Ok(())
|
|
} else {
|
|
Err(DbError {
|
|
kind: DbErrorKind::UpdateError,
|
|
source: None,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn get_preview(&mut self, file_path_val: &str) -> Result<Option<VideoPreviewClip>, DbError> {
|
|
Ok(self.clips.lock().unwrap().get(file_path_val).cloned())
|
|
}
|
|
|
|
fn get_previews_batch(
|
|
&mut self,
|
|
file_paths: &[String],
|
|
) -> Result<Vec<VideoPreviewClip>, DbError> {
|
|
let clips = self.clips.lock().unwrap();
|
|
Ok(file_paths
|
|
.iter()
|
|
.filter_map(|p| clips.get(p).cloned())
|
|
.collect())
|
|
}
|
|
|
|
fn get_by_status(&mut self, status_val: &str) -> Result<Vec<VideoPreviewClip>, DbError> {
|
|
let clips = self.clips.lock().unwrap();
|
|
Ok(clips
|
|
.values()
|
|
.filter(|c| c.status == status_val)
|
|
.cloned()
|
|
.collect())
|
|
}
|
|
}
|