chore: logging

This commit is contained in:
zhom committed 2026-05-10 06:03:48 +04:00
1 parent bc3c2c8cca
commit 8dc48ef526
3 files changed
+96 -12

No files matched your search

+53 -3
View File
@@ -401,6 +401,10 @@ impl ApiServer {
.merge(v1_routes) .merge(v1_routes)
.nest("/ws", ws_routes) .nest("/ws", ws_routes)
.route("/openapi.json", get(move || async move { Json(api) })) .route("/openapi.json", get(move || async move { Json(api) }))
// Outermost layer: logs every request so customer reports show what
// their automation is actually calling, what the response status was,
// and how long it took. Never logs request bodies or auth headers.
.layer(middleware::from_fn(request_logging_middleware))
.layer(CorsLayer::permissive()) .layer(CorsLayer::permissive())
.with_state(state); .with_state(state);
@@ -454,6 +458,8 @@ async fn auth_middleware(
request: axum::extract::Request, request: axum::extract::Request,
next: Next, next: Next,
) -> Result<Response, StatusCode> { ) -> Result<Response, StatusCode> {
let path = request.uri().path().to_string();
// Get the Authorization header // Get the Authorization header
let auth_header = headers let auth_header = headers
.get("Authorization") .get("Authorization")
@@ -462,19 +468,31 @@ async fn auth_middleware(
let token = match auth_header { let token = match auth_header {
Some(token) => token, Some(token) => token,
None => return Err(StatusCode::UNAUTHORIZED), None => {
log::warn!("[api] Rejected {path}: missing Authorization header");
return Err(StatusCode::UNAUTHORIZED);
}
}; };
// Get the stored token // Get the stored token
let settings_manager = crate::settings_manager::SettingsManager::instance(); let settings_manager = crate::settings_manager::SettingsManager::instance();
let stored_token = match settings_manager.get_api_token(&state.app_handle).await { let stored_token = match settings_manager.get_api_token(&state.app_handle).await {
Ok(Some(stored_token)) => stored_token, Ok(Some(stored_token)) => stored_token,
Ok(None) => return Err(StatusCode::UNAUTHORIZED), Ok(None) => {
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), log::warn!(
"[api] Rejected {path}: API server has no stored token (was the API toggled off?)"
);
return Err(StatusCode::UNAUTHORIZED);
}
Err(e) => {
log::error!("[api] Failed to read stored API token: {e}");
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
}; };
// Compare tokens // Compare tokens
if token != stored_token { if token != stored_token {
log::warn!("[api] Rejected {path}: token mismatch");
return Err(StatusCode::UNAUTHORIZED); return Err(StatusCode::UNAUTHORIZED);
} }
@@ -482,6 +500,38 @@ async fn auth_middleware(
Ok(next.run(request).await) Ok(next.run(request).await)
} }
/// Logs every request: method, path, query, response status, duration.
/// Skips Authorization header and request bodies entirely.
async fn request_logging_middleware(request: axum::extract::Request, next: Next) -> Response {
let method = request.method().clone();
let path = request.uri().path().to_string();
let query = request.uri().query().map(|q| q.to_string());
let started = std::time::Instant::now();
let response = next.run(request).await;
let status = response.status();
let elapsed_ms = started.elapsed().as_millis();
let level = if status.is_server_error() {
log::Level::Error
} else if status.is_client_error() {
log::Level::Warn
} else {
log::Level::Info
};
match query {
Some(q) => log::log!(
level,
"[api] {method} {path}?{q} -> {status} ({elapsed_ms} ms)"
),
None => log::log!(level, "[api] {method} {path} -> {status} ({elapsed_ms} ms)"),
}
response
}
// Global API server instance // Global API server instance
lazy_static! { lazy_static! {
pub static ref API_SERVER: Arc<Mutex<ApiServer>> = Arc::new(Mutex::new(ApiServer::new())); pub static ref API_SERVER: Arc<Mutex<ApiServer>> = Arc::new(Mutex::new(ApiServer::new()));
+22 -9
View File
@@ -1350,18 +1350,31 @@ pub fn run() {
version_updater::VersionUpdater::run_background_task().await; version_updater::VersionUpdater::run_background_task().await;
}); });
// Auto-start MCP server if it was previously enabled // Auto-start MCP server if it was previously enabled. Always log the
// decision so customer logs reveal whether MCP is actually running —
// "automation features don't work" is otherwise indistinguishable from
// "MCP server isn't enabled" without this line.
{ {
let mcp_handle = app.handle().clone(); let mcp_handle = app.handle().clone();
let settings_mgr = settings_manager::SettingsManager::instance(); let settings_mgr = settings_manager::SettingsManager::instance();
if let Ok(settings) = settings_mgr.load_settings() { match settings_mgr.load_settings() {
if settings.mcp_enabled { Ok(settings) => {
tauri::async_runtime::spawn(async move { if settings.mcp_enabled {
match mcp_server::McpServer::instance().start(mcp_handle).await { log::info!("MCP server is enabled in settings, attempting auto-start");
Ok(port) => log::info!("MCP server auto-started on port {port}"), tauri::async_runtime::spawn(async move {
Err(e) => log::warn!("Failed to auto-start MCP server: {e}"), match mcp_server::McpServer::instance().start(mcp_handle).await {
} Ok(port) => log::info!("MCP server auto-started on port {port}"),
}); Err(e) => log::warn!("Failed to auto-start MCP server: {e}"),
}
});
} else {
log::info!(
"MCP server is DISABLED in settings (mcp_enabled=false). Browser automation tools will not be available until it's enabled in Settings → Integrations."
);
}
}
Err(e) => {
log::warn!("Could not read settings to determine MCP state: {e}");
} }
} }
} }
+21
View File
@@ -112,6 +112,17 @@ impl McpServer {
async fn require_paid_subscription(feature: &str) -> Result<(), McpError> { async fn require_paid_subscription(feature: &str) -> Result<(), McpError> {
if !CLOUD_AUTH.has_active_paid_subscription().await { if !CLOUD_AUTH.has_active_paid_subscription().await {
// Log the failed gate so customer logs explain why an MCP tool returned
// an error. Include enough state (logged-in vs not, plan, status) for
// support to diagnose without leaking secrets.
let summary = match CLOUD_AUTH.get_user().await {
Some(state) => format!(
"logged_in=true plan={} status={} period={:?}",
state.user.plan, state.user.subscription_status, state.user.plan_period,
),
None => "logged_in=false".to_string(),
};
log::warn!("[mcp] Rejected '{feature}' — paid subscription gate failed ({summary})");
return Err(McpError { return Err(McpError {
code: -32000, code: -32000,
message: format!("{feature} requires an active paid subscription"), message: format!("{feature} requires an active paid subscription"),
@@ -1458,6 +1469,16 @@ impl McpServer {
.cloned() .cloned()
.unwrap_or(serde_json::json!({})); .unwrap_or(serde_json::json!({}));
// Surface the call in logs so customer reports show which tools the MCP
// client is actually invoking (and therefore which gate any subsequent
// error came from). Log only the tool name and the profile_id arg —
// arbitrary URLs / JS / selectors can be sensitive.
let profile_id = arguments
.get("profile_id")
.and_then(|v| v.as_str())
.unwrap_or("<none>");
log::info!("[mcp] tools/call name={tool_name} profile_id={profile_id}");
match tool_name { match tool_name {
"list_profiles" => self.handle_list_profiles().await, "list_profiles" => self.handle_list_profiles().await,
"get_profile" => self.handle_get_profile(&arguments).await, "get_profile" => self.handle_get_profile(&arguments).await,