[Add] Structured Iota event logging

This commit is contained in:
Alex-Emmet 2026-09-25 21:27:05 +02:00
commit 8d576df557
No known key found for this signature in database
8 changed files with 920 additions and 278 deletions

View file

@ -1,5 +1,5 @@
use crate::message_common::*; use crate::message_common::*;
use iota_logger::log; use iota_logger::{LogLevel, PrintType, log_event};
use iota_storage::util::chat_files::{self, MessageState}; use iota_storage::util::chat_files::{self, MessageState};
use iota_storage::util::chats_util::{self, get_user, has_user, mod_user}; use iota_storage::util::chats_util::{self, get_user, has_user, mod_user};
use iota_storage::util::communities_util::CommunitiesUtil; use iota_storage::util::communities_util::CommunitiesUtil;
@ -19,6 +19,24 @@ use std::sync::atomic::{AtomicU32, Ordering};
static NEXT_NOTIFICATION_ID: AtomicU32 = AtomicU32::new(1); static NEXT_NOTIFICATION_ID: AtomicU32 = AtomicU32::new(1);
fn internal_handler_error(
cv: &CommunicationValue,
operation: &'static str,
error: impl std::fmt::Display,
) -> CommunicationValue {
log_event!(
PrintType::Client,
LogLevel::Error,
"request.internal_error",
"operation={} message_id={:?} sender={:?} error={}",
operation,
cv.id(),
cv.sender(),
error
);
error_response(cv, CommunicationType::ErrorInternal)
}
fn next_notification_id() -> u32 { fn next_notification_id() -> u32 {
NEXT_NOTIFICATION_ID.fetch_add(1, Ordering::Relaxed).max(1) NEXT_NOTIFICATION_ID.fetch_add(1, Ordering::Relaxed).max(1)
} }
@ -915,14 +933,14 @@ pub fn handle_get_chat_secret(cv: &CommunicationValue) -> CommunicationValue {
None => return error_response(cv, CommunicationType::ErrorNotFound), None => return error_response(cv, CommunicationType::ErrorNotFound),
}, },
Ok(None) => return error_response(cv, CommunicationType::ErrorNotFound), Ok(None) => return error_response(cv, CommunicationType::ErrorNotFound),
Err(_) => return error_response(cv, CommunicationType::ErrorInternal), Err(error) => return internal_handler_error(cv, "chat_secret.contact_get", error),
}; };
let owner_principal = match iota_storage::identity::SqlitePrincipalStore let owner_principal = match iota_storage::identity::SqlitePrincipalStore
.principal_for_local_user(iota_identity::LocalUserId(sender_id)) .principal_for_local_user(iota_identity::LocalUserId(sender_id))
{ {
Ok(Some(principal)) => principal, Ok(Some(principal)) => principal,
Ok(None) => return error_response(cv, CommunicationType::ErrorNotFound), Ok(None) => return error_response(cv, CommunicationType::ErrorNotFound),
Err(_) => return error_response(cv, CommunicationType::ErrorInternal), Err(error) => return internal_handler_error(cv, "chat_secret.principal_get", error),
}; };
let Some(principal_chat_id) = let Some(principal_chat_id) =
e2ee_storage::principal_chat_id(owner_principal, partner_principal) e2ee_storage::principal_chat_id(owner_principal, partner_principal)
@ -1131,15 +1149,15 @@ pub fn handle_account_state_request(cv: &CommunicationValue) -> CommunicationVal
}; };
let contacts = match chats_util::get_users(user_id) { let contacts = match chats_util::get_users(user_id) {
Ok(contacts) => contacts, Ok(contacts) => contacts,
Err(_) => return error_response(cv, CommunicationType::ErrorInternal), Err(error) => return internal_handler_error(cv, "account_state.snapshot", error),
}; };
let messages = match chat_files::get_all_messages(user_id) { let messages = match chat_files::get_all_messages(user_id) {
Ok(messages) => messages, Ok(messages) => messages,
Err(_) => return error_response(cv, CommunicationType::ErrorInternal), Err(error) => return internal_handler_error(cv, "account_state.snapshot", error),
}; };
let settings = match synced_settings::list(user_id) { let settings = match synced_settings::list(user_id) {
Ok(settings) => settings, Ok(settings) => settings,
Err(_) => return error_response(cv, CommunicationType::ErrorInternal), Err(error) => return internal_handler_error(cv, "account_state.snapshot", error),
}; };
let message_values = messages let message_values = messages
.iter() .iter()
@ -1147,23 +1165,23 @@ pub fn handle_account_state_request(cv: &CommunicationValue) -> CommunicationVal
.collect(); .collect();
let blocked_users = match blocked_users::list(user_id) { let blocked_users = match blocked_users::list(user_id) {
Ok(users) => users, Ok(users) => users,
Err(_) => return error_response(cv, CommunicationType::ErrorInternal), Err(error) => return internal_handler_error(cv, "account_state.snapshot", error),
}; };
let receipt_policy = match receipt_policy::get(user_id) { let receipt_policy = match receipt_policy::get(user_id) {
Ok(policy) => policy, Ok(policy) => policy,
Err(_) => return error_response(cv, CommunicationType::ErrorInternal), Err(error) => return internal_handler_error(cv, "account_state.snapshot", error),
}; };
let message_storage_policy = match message_storage_policy::get(user_id) { let message_storage_policy = match message_storage_policy::get(user_id) {
Ok(policy) => policy, Ok(policy) => policy,
Err(_) => return error_response(cv, CommunicationType::ErrorInternal), Err(error) => return internal_handler_error(cv, "account_state.snapshot", error),
}; };
let contact_ids = match current_contact_ids(user_id) { let contact_ids = match current_contact_ids(user_id) {
Ok(contact_ids) => contact_ids, Ok(contact_ids) => contact_ids,
Err(_) => return error_response(cv, CommunicationType::ErrorInternal), Err(error) => return internal_handler_error(cv, "account_state.snapshot", error),
}; };
let communities = match community_values(user_id) { let communities = match community_values(user_id) {
Ok(communities) => communities, Ok(communities) => communities,
Err(_) => return error_response(cv, CommunicationType::ErrorInternal), Err(error) => return internal_handler_error(cv, "account_state.snapshot", error),
}; };
let retention_duration = match message_storage_policy.retention { let retention_duration = match message_storage_policy.retention {
message_storage_policy::MessageRetention::Forever => None, message_storage_policy::MessageRetention::Forever => None,
@ -1284,7 +1302,7 @@ pub fn handle_messages_get(cv: &CommunicationValue) -> CommunicationValue {
}; };
let messages = match chat_files::get_messages(my_id_i64, partner_id, offset, amount) { let messages = match chat_files::get_messages(my_id_i64, partner_id, offset, amount) {
Ok(messages) => messages, Ok(messages) => messages,
Err(_) => return error_response(cv, CommunicationType::ErrorInternal), Err(error) => return internal_handler_error(cv, "messages.get", error),
}; };
let mut msg_array: Vec<DataValue> = Vec::new(); let mut msg_array: Vec<DataValue> = Vec::new();
for m in &messages { for m in &messages {
@ -1312,12 +1330,12 @@ pub fn handle_message_get(cv: &CommunicationValue) -> CommunicationValue {
{ {
Ok(Some((message, offset))) => (message, Some(offset)), Ok(Some((message, offset))) => (message, Some(offset)),
Ok(None) => return error_response(cv, CommunicationType::ErrorNotFound), Ok(None) => return error_response(cv, CommunicationType::ErrorNotFound),
Err(_) => return error_response(cv, CommunicationType::ErrorInternal), Err(error) => return internal_handler_error(cv, "message.get", error),
}, },
None => match chat_files::get_message(owner, send_time, None, None) { None => match chat_files::get_message(owner, send_time, None, None) {
Ok(Some(message)) => (message, None), Ok(Some(message)) => (message, None),
Ok(None) => return error_response(cv, CommunicationType::ErrorNotFound), Ok(None) => return error_response(cv, CommunicationType::ErrorNotFound),
Err(_) => return error_response(cv, CommunicationType::ErrorInternal), Err(error) => return internal_handler_error(cv, "message.get", error),
}, },
}; };
@ -1344,7 +1362,7 @@ pub fn handle_get_chats(cv: &CommunicationValue) -> CommunicationValue {
}; };
let users = match chats_util::get_users(user_id_i64) { let users = match chats_util::get_users(user_id_i64) {
Ok(users) => users, Ok(users) => users,
Err(_) => return error_response(cv, CommunicationType::ErrorInternal), Err(error) => return internal_handler_error(cv, "chats.get", error),
}; };
let mut user_array = Vec::new(); let mut user_array = Vec::new();
for user in users { for user in users {
@ -1554,8 +1572,8 @@ pub fn handle_global_settings_save(cv: &CommunicationValue) -> CommunicationValu
); );
}; };
if settings::save_global(my_id_i64, settings_value).is_err() { if let Err(error) = settings::save_global(my_id_i64, settings_value) {
return error_response(cv, CommunicationType::ErrorInternal); return internal_handler_error(cv, "settings.global_save", error);
} }
let mut response = CommunicationValue::new(CommunicationType::GlobalSettingsSave) let mut response = CommunicationValue::new(CommunicationType::GlobalSettingsSave)
@ -1702,8 +1720,8 @@ pub fn handle_settings_save(
return error_response(cv, CommunicationType::ErrorInvalidData); return error_response(cv, CommunicationType::ErrorInvalidData);
}; };
if settings::save(my_id_i64, session_id_i64, settings_name, settings_value).is_err() { if let Err(error) = settings::save(my_id_i64, session_id_i64, settings_name, settings_value) {
return error_response(cv, CommunicationType::ErrorInternal); return internal_handler_error(cv, "settings.save", error);
} }
CommunicationValue::new(CommunicationType::SettingsSave) CommunicationValue::new(CommunicationType::SettingsSave)
@ -2085,17 +2103,23 @@ fn upload_response(
} }
fn asset_error(cv: &CommunicationValue, error: StorageError) -> CommunicationValue { fn asset_error(cv: &CommunicationValue, error: StorageError) -> CommunicationValue {
log!( let (kind, category) = match &error {
"Asset request rejected sender={:?} request_id={:?}: {error}",
cv.sender(),
cv.id()
);
let (kind, category) = match error {
StorageError::AssetResourceLimit(_) => { StorageError::AssetResourceLimit(_) => {
(CommunicationType::ErrorInvalidData, "asset_resource_limit") (CommunicationType::ErrorInvalidData, "asset_resource_limit")
} }
_ => (CommunicationType::ErrorInternal, "asset_request_failed"), _ => (CommunicationType::ErrorInternal, "asset_request_failed"),
}; };
if kind == CommunicationType::ErrorInternal {
log_event!(
PrintType::Client,
LogLevel::Error,
"request.internal_error",
"operation=user_assets message_id={:?} sender={:?} error={}",
cv.id(),
cv.sender(),
error
);
}
error_response(cv, kind).add_typed_default(DataType::ErrorType, DataValue::Str(category.into())) error_response(cv, kind).add_typed_default(DataType::ErrorType, DataValue::Str(category.into()))
} }
@ -2212,6 +2236,7 @@ fn parse_setting_locator(cv: &CommunicationValue) -> Result<SettingLocator, Comm
} }
fn validate_setting_target( fn validate_setting_target(
cv: &CommunicationValue,
user_id: i64, user_id: i64,
locator: &SettingLocator, locator: &SettingLocator,
) -> Result<(), CommunicationType> { ) -> Result<(), CommunicationType> {
@ -2225,14 +2250,20 @@ fn validate_setting_target(
match has_user(user_id, contact_id) { match has_user(user_id, contact_id) {
Ok(true) => Ok(()), Ok(true) => Ok(()),
Ok(false) => Err(CommunicationType::ErrorInvalidData), Ok(false) => Err(CommunicationType::ErrorInvalidData),
Err(_) => Err(CommunicationType::ErrorInternal), Err(error) => {
internal_handler_error(cv, "settings.validate_contact", error);
Err(CommunicationType::ErrorInternal)
}
} }
} }
SettingScope::Community => { SettingScope::Community => {
match CommunitiesUtil::has_community(user_id, &locator.scope_key) { match CommunitiesUtil::has_community(user_id, &locator.scope_key) {
Ok(true) => Ok(()), Ok(true) => Ok(()),
Ok(false) => Err(CommunicationType::ErrorInvalidData), Ok(false) => Err(CommunicationType::ErrorInvalidData),
Err(_) => Err(CommunicationType::ErrorInternal), Err(error) => {
internal_handler_error(cv, "settings.validate_community", error);
Err(CommunicationType::ErrorInternal)
}
} }
} }
} }
@ -2262,7 +2293,7 @@ pub fn handle_synced_setting_set(cv: &CommunicationValue) -> SettingMutation {
}; };
} }
}; };
if let Err(error_type) = validate_setting_target(user_id, &locator) { if let Err(error_type) = validate_setting_target(cv, user_id, &locator) {
return setting_mutation_error(cv, error_type); return setting_mutation_error(cv, error_type);
} }
let Some(payload) = cv.get_data(DataType::Payload).as_str() else { let Some(payload) = cv.get_data(DataType::Payload).as_str() else {
@ -2279,7 +2310,10 @@ pub fn handle_synced_setting_set(cv: &CommunicationValue) -> SettingMutation {
response: setting_response(cv, CommunicationType::SyncedSettingSet, &setting), response: setting_response(cv, CommunicationType::SyncedSettingSet, &setting),
changed: Some(setting_changed(&setting)), changed: Some(setting_changed(&setting)),
}, },
Err(_) => setting_mutation_error(cv, CommunicationType::ErrorInternal), Err(error) => SettingMutation {
response: internal_handler_error(cv, "settings.set", error),
changed: None,
},
} }
} }
@ -2292,13 +2326,13 @@ pub fn handle_synced_setting_get(cv: &CommunicationValue) -> CommunicationValue
Ok(locator) => locator, Ok(locator) => locator,
Err(response) => return response, Err(response) => return response,
}; };
if let Err(error_type) = validate_setting_target(user_id, &locator) { if let Err(error_type) = validate_setting_target(cv, user_id, &locator) {
return error_response(cv, error_type); return error_response(cv, error_type);
} }
match synced_settings::get(user_id, locator.scope, &locator.scope_key, &locator.name) { match synced_settings::get(user_id, locator.scope, &locator.scope_key, &locator.name) {
Ok(Some(setting)) => setting_response(cv, CommunicationType::SyncedSettingGet, &setting), Ok(Some(setting)) => setting_response(cv, CommunicationType::SyncedSettingGet, &setting),
Ok(None) => error_response(cv, CommunicationType::ErrorNotFound), Ok(None) => error_response(cv, CommunicationType::ErrorNotFound),
Err(_) => error_response(cv, CommunicationType::ErrorInternal), Err(error) => internal_handler_error(cv, "settings.get", error),
} }
} }
@ -2316,7 +2350,7 @@ pub fn handle_synced_setting_delete(cv: &CommunicationValue) -> SettingMutation
}; };
} }
}; };
if let Err(error_type) = validate_setting_target(user_id, &locator) { if let Err(error_type) = validate_setting_target(cv, user_id, &locator) {
return setting_mutation_error(cv, error_type); return setting_mutation_error(cv, error_type);
} }
match synced_settings::delete(user_id, locator.scope, &locator.scope_key, &locator.name) { match synced_settings::delete(user_id, locator.scope, &locator.scope_key, &locator.name) {
@ -2340,7 +2374,10 @@ pub fn handle_synced_setting_delete(cv: &CommunicationValue) -> SettingMutation
.with_receiver(sender_wire_id(user_id)), .with_receiver(sender_wire_id(user_id)),
changed: None, changed: None,
}, },
Err(_) => setting_mutation_error(cv, CommunicationType::ErrorInternal), Err(error) => SettingMutation {
response: internal_handler_error(cv, "settings.delete", error),
changed: None,
},
} }
} }
@ -2357,7 +2394,7 @@ pub fn handle_synced_settings_list(cv: &CommunicationValue) -> CommunicationValu
DataType::Settings, DataType::Settings,
DataValue::Array(settings.iter().map(synced_setting_value).collect()), DataValue::Array(settings.iter().map(synced_setting_value).collect()),
), ),
Err(_) => error_response(cv, CommunicationType::ErrorInternal), Err(error) => internal_handler_error(cv, "settings.list", error),
} }
} }
@ -2384,8 +2421,8 @@ pub fn handle_user_blob_put(cv: &CommunicationValue) -> BlobMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData), response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None, changed: None,
}, },
Err(_) => BlobMutation { Err(error) => BlobMutation {
response: error_response(cv, CommunicationType::ErrorInternal), response: internal_handler_error(cv, "user_blobs.put", error),
changed: None, changed: None,
}, },
} }
@ -2398,7 +2435,7 @@ pub fn handle_user_blob_get(cv: &CommunicationValue) -> CommunicationValue {
match user_blobs::get(user_id, &blob_id) { match user_blobs::get(user_id, &blob_id) {
Ok(Some(blob)) => blob_response(cv, CommunicationType::UserBlobGet, &blob), Ok(Some(blob)) => blob_response(cv, CommunicationType::UserBlobGet, &blob),
Ok(None) => error_response(cv, CommunicationType::ErrorNotFound), Ok(None) => error_response(cv, CommunicationType::ErrorNotFound),
Err(_) => error_response(cv, CommunicationType::ErrorInternal), Err(error) => internal_handler_error(cv, "user_blobs.get", error),
} }
} }
@ -2433,8 +2470,8 @@ pub fn handle_user_blob_delete(cv: &CommunicationValue) -> BlobMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData), response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None, changed: None,
}, },
Err(_) => BlobMutation { Err(error) => BlobMutation {
response: error_response(cv, CommunicationType::ErrorInternal), response: internal_handler_error(cv, "user_blobs.delete", error),
changed: None, changed: None,
}, },
} }
@ -2466,7 +2503,7 @@ pub fn handle_user_blob_list(cv: &CommunicationValue) -> CommunicationValue {
DataType::Blobs, DataType::Blobs,
DataValue::Array(blobs.iter().map(blob_metadata_value).collect()), DataValue::Array(blobs.iter().map(blob_metadata_value).collect()),
), ),
Err(_) => error_response(cv, CommunicationType::ErrorInternal), Err(error) => internal_handler_error(cv, "user_blobs.list", error),
} }
} }
@ -2719,8 +2756,8 @@ pub fn handle_user_block(cv: &CommunicationValue) -> PolicyMutation {
.add_typed_default(DataType::Deleted, DataValue::Bool(record.deleted)), .add_typed_default(DataType::Deleted, DataValue::Bool(record.deleted)),
), ),
}, },
Err(_) => PolicyMutation { Err(error) => PolicyMutation {
response: error_response(cv, CommunicationType::ErrorInternal), response: internal_handler_error(cv, "blocked_users.block", error),
changed: None, changed: None,
}, },
} }
@ -2775,8 +2812,8 @@ pub fn handle_user_unblock(cv: &CommunicationValue) -> PolicyMutation {
.add_typed_default(DataType::Deleted, DataValue::Bool(mutation.deleted)) .add_typed_default(DataType::Deleted, DataValue::Bool(mutation.deleted))
}), }),
}, },
Err(_) => PolicyMutation { Err(error) => PolicyMutation {
response: error_response(cv, CommunicationType::ErrorInternal), response: internal_handler_error(cv, "blocked_users.unblock", error),
changed: None, changed: None,
}, },
} }
@ -2799,7 +2836,7 @@ pub fn handle_blocked_users_get(cv: &CommunicationValue) -> CommunicationValue {
.collect(), .collect(),
), ),
), ),
Err(_) => error_response(cv, CommunicationType::ErrorInternal), Err(error) => internal_handler_error(cv, "blocked_users.get", error),
} }
} }
@ -2819,7 +2856,7 @@ pub fn handle_receipt_policy_get(cv: &CommunicationValue) -> CommunicationValue
}; };
match receipt_policy::get(user_id) { match receipt_policy::get(user_id) {
Ok(policy) => receipt_response(cv, CommunicationType::ReceiptPolicyGet, policy), Ok(policy) => receipt_response(cv, CommunicationType::ReceiptPolicyGet, policy),
Err(_) => error_response(cv, CommunicationType::ErrorInternal), Err(error) => internal_handler_error(cv, "receipt_policy.get", error),
} }
} }
pub fn handle_receipt_policy_set(cv: &CommunicationValue) -> PolicyMutation { pub fn handle_receipt_policy_set(cv: &CommunicationValue) -> PolicyMutation {
@ -2850,8 +2887,8 @@ pub fn handle_receipt_policy_set(cv: &CommunicationValue) -> PolicyMutation {
changed: Some(changed), changed: Some(changed),
} }
} }
Err(_) => PolicyMutation { Err(error) => PolicyMutation {
response: error_response(cv, CommunicationType::ErrorInternal), response: internal_handler_error(cv, "receipt_policy.set", error),
changed: None, changed: None,
}, },
} }
@ -2900,7 +2937,7 @@ pub fn handle_message_storage_policy_get(cv: &CommunicationValue) -> Communicati
Ok(policy) => { Ok(policy) => {
storage_policy_response(cv, CommunicationType::MessageStoragePolicyGet, policy) storage_policy_response(cv, CommunicationType::MessageStoragePolicyGet, policy)
} }
Err(_) => error_response(cv, CommunicationType::ErrorInternal), Err(error) => internal_handler_error(cv, "message_storage_policy.get", error),
} }
} }
@ -2948,8 +2985,8 @@ pub fn handle_message_storage_policy_set(cv: &CommunicationValue) -> PolicyMutat
changed: Some(changed), changed: Some(changed),
} }
} }
Err(_) => PolicyMutation { Err(error) => PolicyMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData), response: internal_handler_error(cv, "message_storage_policy.set", error),
changed: None, changed: None,
}, },
} }
@ -2970,13 +3007,13 @@ pub fn handle_user_block_check(cv: &CommunicationValue) -> CommunicationValue {
None => return error_response(cv, CommunicationType::ErrorNotFound), None => return error_response(cv, CommunicationType::ErrorNotFound),
}, },
Ok(None) => return error_response(cv, CommunicationType::ErrorNotFound), Ok(None) => return error_response(cv, CommunicationType::ErrorNotFound),
Err(_) => return error_response(cv, CommunicationType::ErrorInternal), Err(error) => return internal_handler_error(cv, "blocked_users.check_contact", error),
}; };
match blocked_users::is_principal_blocked(receiver_id, sender_principal) { match blocked_users::is_principal_blocked(receiver_id, sender_principal) {
Ok(blocked) => CommunicationValue::new(CommunicationType::UserBlockCheck) Ok(blocked) => CommunicationValue::new(CommunicationType::UserBlockCheck)
.with_request_id(cv) .with_request_id(cv)
.add_typed_default(DataType::IsBlocked, DataValue::Bool(blocked)), .add_typed_default(DataType::IsBlocked, DataValue::Bool(blocked)),
Err(_) => error_response(cv, CommunicationType::ErrorInternal), Err(error) => internal_handler_error(cv, "blocked_users.check", error),
} }
} }

View file

@ -8,7 +8,7 @@ use iota_ipc::{
TaskSummary, UpdateStatusResponse, UserDetailResponse, UserDiagnostics, UserOperationKind, TaskSummary, UpdateStatusResponse, UserDetailResponse, UserDiagnostics, UserOperationKind,
UserOperationSummary, UserReconcileResult, UserSummary, UserOperationSummary, UserReconcileResult, UserSummary,
}; };
use iota_logger::{log, log_command}; use iota_logger::{LogLevel, PrintType, log, log_command, log_event};
use iota_storage::users::pending_operations::{self, PendingUserOperationKind}; use iota_storage::users::pending_operations::{self, PendingUserOperationKind};
use iota_storage::users::user_manager; use iota_storage::users::user_manager;
use iota_storage::util::config_util::{self}; use iota_storage::util::config_util::{self};
@ -64,6 +64,7 @@ fn now_millis() -> i64 {
} }
fn invitation_created_after_mirror( fn invitation_created_after_mirror(
request_id: u64,
authority: InvitationAuthority, authority: InvitationAuthority,
invitation_id: i64, invitation_id: i64,
raw_token: String, raw_token: String,
@ -74,8 +75,12 @@ fn invitation_created_after_mirror(
let mirror_synced = match mirror_result { let mirror_synced = match mirror_result {
Ok(()) => true, Ok(()) => true,
Err(error) => { Err(error) => {
log!( log_event!(
"Invitation {} was created by Omega, but its local mirror could not be stored: {}", PrintType::Command,
LogLevel::Error,
"invitation.mirror_store_failed",
"request_id={} invitation_id={} error={}",
request_id,
invitation_id, invitation_id,
error error
); );
@ -139,12 +144,16 @@ impl CommandRouter {
request: LocalRequest, request: LocalRequest,
) -> ResponseEnvelope { ) -> ResponseEnvelope {
if !peer.role.allows(request.required_role()) { if !peer.role.allows(request.required_role()) {
log!( log_event!(
"IPC authorization denied: pid={}, uid={}, role={:?}, request={:?}", PrintType::Command,
LogLevel::Warn,
"ipc.authorization_denied",
"request_id={} pid={} uid={} role={:?} request={}",
request_id,
peer.pid, peer.pid,
peer.uid, peer.uid,
peer.role, peer.role,
request request.log_name()
); );
return ResponseEnvelope { return ResponseEnvelope {
request_id, request_id,
@ -153,17 +162,18 @@ impl CommandRouter {
} }
log_command!( log_command!(
"pid={} uid={} role={:?} request={:?}", "request_id={} pid={} uid={} role={:?} request={}",
request_id,
peer.pid, peer.pid,
peer.uid, peer.uid,
peer.role, peer.role,
request request.log_name()
); );
let result = self.execute(request).await; let result = self.execute(request_id, request).await;
ResponseEnvelope { request_id, result } ResponseEnvelope { request_id, result }
} }
async fn execute(&self, request: LocalRequest) -> ResponseResult { async fn execute(&self, request_id: u64, request: LocalRequest) -> ResponseResult {
if !self.services.active if !self.services.active
&& !matches!( && !matches!(
request, request,
@ -248,11 +258,31 @@ impl CommandRouter {
) )
}) })
.collect::<std::collections::HashMap<_, _>>(), .collect::<std::collections::HashMap<_, _>>(),
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure), Err(error) => {
log_event!(
PrintType::Command,
LogLevel::Error,
"user.pending_operations_load_failed",
"request_id={} error={}",
request_id,
error
);
return ResponseResult::Error(IpcErrorCode::StorageFailure);
}
}; };
let users = match user_manager::get_residency() { let users = match user_manager::get_residency() {
Ok(users) => users, Ok(users) => users,
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure), Err(error) => {
log_event!(
PrintType::Command,
LogLevel::Error,
"user.residency_load_failed",
"request_id={} error={}",
request_id,
error
);
return ResponseResult::Error(IpcErrorCode::StorageFailure);
}
} }
.into_iter() .into_iter()
.map(|user| { .map(|user| {
@ -275,8 +305,19 @@ impl CommandRouter {
}) })
}) })
.collect::<Result<Vec<_>, iota_storage::storage_error::StorageError>>(); .collect::<Result<Vec<_>, iota_storage::storage_error::StorageError>>();
let Ok(users) = users else { let users = match users {
Ok(users) => users,
Err(error) => {
log_event!(
PrintType::Command,
LogLevel::Error,
"user.profile_load_failed",
"request_id={} error={}",
request_id,
error
);
return ResponseResult::Error(IpcErrorCode::StorageFailure); return ResponseResult::Error(IpcErrorCode::StorageFailure);
}
}; };
ResponseResult::Ok(ResponsePayload::Users(users)) ResponseResult::Ok(ResponsePayload::Users(users))
} }
@ -386,7 +427,17 @@ impl CommandRouter {
Err(omikron_connector::OmikronError::Timeout(_)) => { Err(omikron_connector::OmikronError::Timeout(_)) => {
return ResponseResult::Error(IpcErrorCode::Timeout); return ResponseResult::Error(IpcErrorCode::Timeout);
} }
Err(_) => return ResponseResult::Error(IpcErrorCode::OmikronUnavailable), Err(error) => {
log_event!(
PrintType::Command,
LogLevel::Warn,
"invitation.create_failed",
"request_id={} error={}",
request_id,
error
);
return ResponseResult::Error(IpcErrorCode::OmikronUnavailable);
}
}; };
let invitation_id = response let invitation_id = response
.get_data(DataType::InvitationId) .get_data(DataType::InvitationId)
@ -419,6 +470,13 @@ impl CommandRouter {
.zip(expires_at) .zip(expires_at)
.map(|(((id, token), created), expires)| (id, token, created, expires)) .map(|(((id, token), created), expires)| (id, token, created, expires))
else { else {
log_event!(
PrintType::Command,
LogLevel::Error,
"invitation.invalid_response",
"request_id={} reason=missing_required_fields",
request_id
);
return ResponseResult::Error(IpcErrorCode::InternalFailure); return ResponseResult::Error(IpcErrorCode::InternalFailure);
}; };
let summary = iota_storage::users::invitations::InvitationSummary { let summary = iota_storage::users::invitations::InvitationSummary {
@ -443,6 +501,7 @@ impl CommandRouter {
iota_storage::users::invitations::insert(&summary, None, now_millis()); iota_storage::users::invitations::insert(&summary, None, now_millis());
ResponseResult::Ok(ResponsePayload::InvitationCreated( ResponseResult::Ok(ResponsePayload::InvitationCreated(
invitation_created_after_mirror( invitation_created_after_mirror(
request_id,
authority, authority,
invitation_id, invitation_id,
raw_token, raw_token,
@ -471,12 +530,30 @@ impl CommandRouter {
Err(_) => return ResponseResult::Error(IpcErrorCode::InternalFailure), Err(_) => return ResponseResult::Error(IpcErrorCode::InternalFailure),
} }
} }
if iota_storage::users::invitations::expire_pending(now_millis()).is_err() { if let Err(error) = iota_storage::users::invitations::expire_pending(now_millis()) {
log_event!(
PrintType::Command,
LogLevel::Error,
"invitation.expire_failed",
"request_id={} error={}",
request_id,
error
);
return ResponseResult::Error(IpcErrorCode::StorageFailure); return ResponseResult::Error(IpcErrorCode::StorageFailure);
} }
let invitations = match iota_storage::users::invitations::list() { let invitations = match iota_storage::users::invitations::list() {
Ok(invitations) => invitations, Ok(invitations) => invitations,
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure), Err(error) => {
log_event!(
PrintType::Command,
LogLevel::Error,
"invitation.list_failed",
"request_id={} error={}",
request_id,
error
);
return ResponseResult::Error(IpcErrorCode::StorageFailure);
}
}; };
let mut summaries = Vec::new(); let mut summaries = Vec::new();
for invitation in invitations { for invitation in invitations {
@ -875,10 +952,30 @@ impl CommandRouter {
Err(AccountError::Unavailable(_)) => { Err(AccountError::Unavailable(_)) => {
ResponseResult::Error(IpcErrorCode::OmikronUnavailable) ResponseResult::Error(IpcErrorCode::OmikronUnavailable)
} }
Err(AccountError::Storage(_)) => { Err(AccountError::Storage(error)) => {
log_event!(
PrintType::Command,
LogLevel::Error,
"user.release_storage_failed",
"request_id={} user_id={} error={:?}",
request_id,
user_id,
error
);
ResponseResult::Error(IpcErrorCode::StorageFailure) ResponseResult::Error(IpcErrorCode::StorageFailure)
} }
Err(_) => ResponseResult::Error(IpcErrorCode::InternalFailure), Err(error) => {
log_event!(
PrintType::Command,
LogLevel::Error,
"user.release_failed",
"request_id={} user_id={} error={:?}",
request_id,
user_id,
error
);
ResponseResult::Error(IpcErrorCode::InternalFailure)
}
} }
} }
LocalRequest::ReconnectOmikron => match self.services.omikron() { LocalRequest::ReconnectOmikron => match self.services.omikron() {
@ -886,7 +983,17 @@ impl CommandRouter {
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged { Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: "Reconnected to Omikron server".into(), message: "Reconnected to Omikron server".into(),
}), }),
Err(_) => ResponseResult::Error(IpcErrorCode::OmikronUnavailable), Err(error) => {
log_event!(
PrintType::Command,
LogLevel::Warn,
"ipc.omikron_reconnect_failed",
"request_id={} error={}",
request_id,
error
);
ResponseResult::Error(IpcErrorCode::OmikronUnavailable)
}
}, },
None => ResponseResult::Error(IpcErrorCode::OmikronUnavailable), None => ResponseResult::Error(IpcErrorCode::OmikronUnavailable),
}, },
@ -943,17 +1050,48 @@ impl CommandRouter {
} }
LocalRequest::SetConfig { key, value } => { LocalRequest::SetConfig { key, value } => {
match config_util::modify_config_value(&key, &value) { match config_util::modify_config_value(&key, &value) {
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged { Ok(()) => {
message: format!("Set {key} = {value}"), log_event!(
}), PrintType::Command,
Err(_e) => ResponseResult::Error(IpcErrorCode::InvalidRequest), LogLevel::Info,
"config.changed",
"request_id={} key={:?}",
request_id,
key
);
ResponseResult::Ok(ResponsePayload::Acknowledged {
message: format!("Set {key}"),
})
}
Err(error) => {
log_event!(
PrintType::Command,
LogLevel::Warn,
"config.change_failed",
"request_id={} key={:?} error={}",
request_id,
key,
error
);
ResponseResult::Error(IpcErrorCode::InvalidRequest)
}
} }
} }
LocalRequest::ReloadConfig => match config_util::load_config() { LocalRequest::ReloadConfig => match config_util::load_config() {
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged { Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: "Configuration reloaded".into(), message: "Configuration reloaded".into(),
}), }),
Err(_) => ResponseResult::Error(IpcErrorCode::InvalidRequest), Err(error) => {
log_event!(
PrintType::Command,
LogLevel::Warn,
"config.reload_failed",
"request_id={} error={}",
request_id,
error
);
ResponseResult::Error(IpcErrorCode::InvalidRequest)
}
}, },
LocalRequest::GetOmikronStatus => { LocalRequest::GetOmikronStatus => {
let connected = match self.services.omikron() { let connected = match self.services.omikron() {
@ -983,7 +1121,18 @@ impl CommandRouter {
let residency = match user_manager::get_residency_by_id(user_id) { let residency = match user_manager::get_residency_by_id(user_id) {
Ok(Some(residency)) => residency, Ok(Some(residency)) => residency,
Ok(None) => return ResponseResult::Error(IpcErrorCode::NotFound), Ok(None) => return ResponseResult::Error(IpcErrorCode::NotFound),
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure), Err(error) => {
log_event!(
PrintType::Command,
LogLevel::Error,
"user.residency_load_failed",
"request_id={} user_id={} error={}",
request_id,
user_id,
error
);
return ResponseResult::Error(IpcErrorCode::StorageFailure);
}
}; };
match user_manager::get_user(user_id) { match user_manager::get_user(user_id) {
Ok(Some(user)) => { Ok(Some(user)) => {
@ -1016,14 +1165,36 @@ impl CommandRouter {
})) }))
} }
Ok(None) => ResponseResult::Error(IpcErrorCode::StorageFailure), Ok(None) => ResponseResult::Error(IpcErrorCode::StorageFailure),
Err(_) => ResponseResult::Error(IpcErrorCode::StorageFailure), Err(error) => {
log_event!(
PrintType::Command,
LogLevel::Error,
"user.profile_load_failed",
"request_id={} user_id={} error={}",
request_id,
user_id,
error
);
ResponseResult::Error(IpcErrorCode::StorageFailure)
}
} }
} }
LocalRequest::ExportUserCredential { user_id } => { LocalRequest::ExportUserCredential { user_id } => {
let residency = match user_manager::get_residency_by_id(user_id) { let residency = match user_manager::get_residency_by_id(user_id) {
Ok(Some(residency)) => residency, Ok(Some(residency)) => residency,
Ok(None) => return ResponseResult::Error(IpcErrorCode::NotFound), Ok(None) => return ResponseResult::Error(IpcErrorCode::NotFound),
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure), Err(error) => {
log_event!(
PrintType::Command,
LogLevel::Error,
"user.residency_load_failed",
"request_id={} user_id={} error={}",
request_id,
user_id,
error
);
return ResponseResult::Error(IpcErrorCode::StorageFailure);
}
}; };
if residency.state != user_manager::LocalUserState::Managed if residency.state != user_manager::LocalUserState::Managed
|| residency.credential_origin != user_manager::CredentialOrigin::Local || residency.credential_origin != user_manager::CredentialOrigin::Local
@ -1036,7 +1207,18 @@ impl CommandRouter {
) { ) {
Ok(Some(credential)) => credential, Ok(Some(credential)) => credential,
Ok(None) => return ResponseResult::Error(IpcErrorCode::NotFound), Ok(None) => return ResponseResult::Error(IpcErrorCode::NotFound),
Err(_) => return ResponseResult::Error(IpcErrorCode::StorageFailure), Err(error) => {
log_event!(
PrintType::Command,
LogLevel::Error,
"user.credential_read_failed",
"request_id={} user_id={} error={}",
request_id,
user_id,
error
);
return ResponseResult::Error(IpcErrorCode::StorageFailure);
}
}; };
let parsed = match iota_util::tu::TuCredential::parse(&credential) { let parsed = match iota_util::tu::TuCredential::parse(&credential) {
Ok(parsed) if parsed.user_id == user_id => parsed, Ok(parsed) if parsed.user_id == user_id => parsed,
@ -1063,7 +1245,17 @@ impl CommandRouter {
available, available,
})) }))
} }
Err(_e) => ResponseResult::Error(IpcErrorCode::InternalFailure), Err(error) => {
log_event!(
PrintType::Command,
LogLevel::Error,
"update.check_failed",
"request_id={} error={}",
request_id,
error
);
ResponseResult::Error(IpcErrorCode::InternalFailure)
}
}, },
LocalRequest::ListCommunities => { LocalRequest::ListCommunities => {
let iota_id = config_util::CONFIG.load().iota_id; let iota_id = config_util::CONFIG.load().iota_id;
@ -1182,6 +1374,7 @@ mod tests {
#[test] #[test]
fn omega_creation_credentials_survive_a_local_mirror_failure() { fn omega_creation_credentials_survive_a_local_mirror_failure() {
let created = invitation_created_after_mirror( let created = invitation_created_after_mirror(
1,
InvitationAuthority::Omega, InvitationAuthority::Omega,
7, 7,
"raw-token".into(), "raw-token".into(),

View file

@ -1,17 +1,28 @@
use crate::log_buffer::LogBuffer; use crate::log_buffer::LogBuffer;
use iota_ipc::{DaemonMessage, LogEntry}; use iota_ipc::{DaemonMessage, LogEntry};
use iota_logger::subscribe; use iota_logger::subscribe;
use iota_state::UiLogEntry;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tokio::sync::broadcast::error::RecvError;
/* The daemon adapts logger output to the wire protocol so the logger stays /* The daemon adapts logger output to the wire protocol so the logger stays
* independent from both the socket implementation and TUI state. */ * independent from both the socket implementation and TUI state. */
pub fn spawn(message_tx: broadcast::Sender<DaemonMessage>, buffer: Arc<Mutex<LogBuffer>>) { pub fn spawn(message_tx: broadcast::Sender<DaemonMessage>, buffer: Arc<Mutex<LogBuffer>>) {
let Some(mut logs) = subscribe() else { let Some(logs) = subscribe() else {
return; return;
}; };
tokio::spawn(async move { tokio::spawn(forward_logs(logs, message_tx, buffer));
while let Ok(entry) = logs.recv().await { }
pub(crate) async fn forward_logs(
mut logs: broadcast::Receiver<UiLogEntry>,
message_tx: broadcast::Sender<DaemonMessage>,
buffer: Arc<Mutex<LogBuffer>>,
) {
loop {
match logs.recv().await {
Ok(entry) => {
let entry = LogEntry { let entry = LogEntry {
timestamp_ms: entry.timestamp_ms, timestamp_ms: entry.timestamp_ms,
sender: entry.sender, sender: entry.sender,
@ -23,5 +34,62 @@ pub fn spawn(message_tx: broadcast::Sender<DaemonMessage>, buffer: Arc<Mutex<Log
} }
let _ = message_tx.send(DaemonMessage::LogEntry(entry)); let _ = message_tx.send(DaemonMessage::LogEntry(entry));
} }
Err(RecvError::Lagged(skipped)) => {
let _ = message_tx.send(DaemonMessage::Gap { skipped });
}
Err(RecvError::Closed) => break,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn lag_reports_gap_and_continues_forwarding() {
let (logs_tx, logs_rx) = broadcast::channel(2);
let (message_tx, mut messages) = broadcast::channel(16);
let buffer = Arc::new(Mutex::new(LogBuffer::new(8)));
for index in 0..4 {
let _ = logs_tx.send(UiLogEntry {
timestamp_ms: index,
sender: "Client".into(),
message: index.to_string(),
is_error: false,
}); });
}
let task = tokio::spawn(forward_logs(logs_rx, message_tx, buffer.clone()));
assert!(matches!(
messages.recv().await.unwrap(),
DaemonMessage::Gap { skipped: 2 }
));
assert!(matches!(
messages.recv().await.unwrap(),
DaemonMessage::LogEntry(_)
));
let _ = logs_tx.send(UiLogEntry {
timestamp_ms: 4,
sender: "Client".into(),
message: "after lag".into(),
is_error: false,
});
let mut found = false;
for _ in 0..2 {
if let DaemonMessage::LogEntry(entry) = messages.recv().await.unwrap() {
found |= entry.message == "after lag";
}
}
assert!(found);
drop(logs_tx);
task.await.unwrap();
assert!(
buffer
.lock()
.unwrap()
.recent(4)
.iter()
.any(|entry| entry.message == "after lag")
);
}
} }

View file

@ -171,6 +171,52 @@ impl IpcRole {
} }
impl LocalRequest { impl LocalRequest {
pub const fn log_name(&self) -> &'static str {
match self {
Self::GetStatus => "get_status",
Self::ListTasks => "list_tasks",
Self::ListUsers => "list_users",
Self::ListTAuthApps => "list_tauth_apps",
Self::GetTAuthApp { .. } => "get_tauth_app",
Self::CreateTAuthApp { .. } => "create_tauth_app",
Self::ExportTAuthApp { .. } => "export_tauth_app",
Self::DeleteTAuthApp { .. } => "delete_tauth_app",
Self::CreateInvitation { .. } => "create_invitation",
Self::ListInvitations { .. } => "list_invitations",
Self::RevokeInvitation { .. } => "revoke_invitation",
Self::CreateUser { .. } => "create_user",
Self::InspectTuCredential { .. } => "inspect_tu_credential",
Self::AttachUserFromTu { .. } => "attach_user_from_tu",
Self::ReconcileUser { .. } => "reconcile_user",
Self::ForceDetachUser { .. } => "force_detach_user",
Self::ForgetReleasedUser { .. } => "forget_released_user",
Self::GetUserDiagnostics { .. } => "get_user_diagnostics",
Self::RevokeTAuthGrant { .. } => "revoke_tauth_grant",
Self::RevokeAllTAuthGrants { .. } => "revoke_all_tauth_grants",
Self::ExportUserCredential { .. } => "export_user_credential",
Self::PurgeUserData { .. } => "purge_user_data",
Self::ReleaseUser { .. } => "release_user",
Self::CompleteDeleteUser { .. } => "complete_delete_user",
Self::RemoveUser { .. } => "remove_user",
Self::ReconnectOmikron => "reconnect_omikron",
Self::RotateIotaIdentity => "rotate_iota_identity",
Self::RequestProcessExit { .. } => "request_process_exit",
Self::GetDaemonStatus => "get_daemon_status",
Self::RestartDaemon => "restart_daemon",
Self::StopDaemon => "stop_daemon",
Self::GetConfig => "get_config",
Self::SetConfig { .. } => "set_config",
Self::ReloadConfig => "reload_config",
Self::GetOmikronStatus => "get_omikron_status",
Self::ListComponents => "list_components",
Self::GetUser { .. } => "get_user",
Self::ImportUser { .. } => "import_user",
Self::GetLogs { .. } => "get_logs",
Self::CheckUpdate => "check_update",
Self::ListCommunities => "list_communities",
}
}
/// Return the minimum authenticated local role required to execute a /// Return the minimum authenticated local role required to execute a
/// request. New request variants must be assigned explicitly here. /// request. New request variants must be assigned explicitly here.
pub fn required_role(&self) -> IpcRole { pub fn required_role(&self) -> IpcRole {
@ -620,6 +666,16 @@ mod error_tests {
assert!(output.contains("<redacted>")); assert!(output.contains("<redacted>"));
} }
#[test]
fn request_log_name_does_not_include_config_value() {
let request = super::LocalRequest::SetConfig {
key: "omikron_host".into(),
value: "private-config-value".into(),
};
assert_eq!(request.log_name(), "set_config");
assert!(!request.log_name().contains("private-config-value"));
}
#[test] #[test]
fn user_summary_serializes_pending_operation_without_lifecycle_secrets() { fn user_summary_serializes_pending_operation_without_lifecycle_secrets() {
let summary = UserSummary { let summary = UserSummary {

View file

@ -1,12 +1,17 @@
use std::{ use std::{
fs::{self, OpenOptions}, fs::{self, OpenOptions},
io::Write, io::Write,
sync::{OnceLock, atomic::Ordering, mpsc}, path::{Path, PathBuf},
sync::{
OnceLock,
atomic::{AtomicU64, Ordering},
mpsc::{self, RecvTimeoutError, TrySendError},
},
thread, thread,
time::{SystemTime, UNIX_EPOCH}, time::{Duration, Instant, SystemTime, UNIX_EPOCH},
}; };
use mtp::codec::{CommunicationValue, DataTypeId, DataValue, TypeMap, Version}; use mtp::codec::{CommunicationType, CommunicationValue, DataTypeId, DataValue, TypeMap};
use ratatui::style::Color; use ratatui::style::Color;
use iota_state::{UNIQUE, UiLogEntry}; use iota_state::{UNIQUE, UiLogEntry};
@ -14,8 +19,31 @@ use tokio::sync::broadcast;
pub mod language_creator; pub mod language_creator;
pub mod language_manager; pub mod language_manager;
static LOGGER: OnceLock<mpsc::Sender<LogMessage>> = OnceLock::new(); static LOGGER: OnceLock<mpsc::SyncSender<LogMessage>> = OnceLock::new();
static LOG_BROADCASTER: OnceLock<broadcast::Sender<UiLogEntry>> = OnceLock::new(); static LOG_BROADCASTER: OnceLock<broadcast::Sender<UiLogEntry>> = OnceLock::new();
static DROPPED_LOGS: AtomicU64 = AtomicU64::new(0);
const DEFAULT_LOGGER_QUEUE_CAPACITY: usize = 1024;
const DROPPED_LOG_REPORT_INTERVAL: Duration = Duration::from_secs(5);
const MAX_LOG_FILE_BYTES: u64 = 16 * 1024 * 1024;
const RETAINED_LOG_FILES: usize = 8;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
pub const fn as_str(self) -> &'static str {
match self {
Self::Debug => "DEBUG",
Self::Info => "INFO",
Self::Warn => "WARN",
Self::Error => "ERROR",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[allow(unused)] #[allow(unused)]
@ -29,6 +57,17 @@ pub enum PrintType {
Command, Command,
} }
impl PrintType { impl PrintType {
pub const fn as_str(self) -> &'static str {
match self {
Self::Call => "call",
Self::Client => "client",
Self::Iota => "iota",
Self::Omikron => "omikron",
Self::Omega => "omega",
Self::General => "general",
Self::Command => "command",
}
}
pub fn prefix_color(self) -> Color { pub fn prefix_color(self) -> Color {
match self { match self {
PrintType::Call => Color::Magenta, PrintType::Call => Color::Magenta,
@ -47,6 +86,8 @@ struct LogMessage {
prefix: String, prefix: String,
kind: PrintType, kind: PrintType,
is_error: bool, is_error: bool,
level: LogLevel,
event: &'static str,
translation_key: Option<String>, translation_key: Option<String>,
format_args: Vec<String>, format_args: Vec<String>,
message: Option<String>, message: Option<String>,
@ -55,16 +96,23 @@ struct LogMessage {
/* The logger owns file persistence while consumers receive rendered entries /* The logger owns file persistence while consumers receive rendered entries
* through a process-local broadcast subscription. */ * through a process-local broadcast subscription. */
pub fn startup() { pub fn startup() {
startup_with_log_dir(Some( startup_with_log_dir_and_capacity(
Some(
iota_paths::IotaPaths::resolve(iota_paths::Scope::User) iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
.expect("resolve Iota user paths") .expect("resolve Iota user paths")
.log_dir, .log_dir,
)); ),
DEFAULT_LOGGER_QUEUE_CAPACITY,
);
} }
/// `None` keeps logging on stderr only (the systemd default). /// `None` keeps logging on stderr only (the systemd default).
pub fn startup_with_log_dir(log_dir: Option<std::path::PathBuf>) { pub fn startup_with_log_dir(log_dir: Option<std::path::PathBuf>) {
let (tx, rx) = mpsc::channel::<LogMessage>(); startup_with_log_dir_and_capacity(log_dir, DEFAULT_LOGGER_QUEUE_CAPACITY);
}
pub fn startup_with_log_dir_and_capacity(log_dir: Option<PathBuf>, queue_capacity: usize) {
let (tx, rx) = mpsc::sync_channel::<LogMessage>(queue_capacity.max(1));
if LOGGER.set(tx).is_err() { if LOGGER.set(tx).is_err() {
return; return;
} }
@ -72,17 +120,45 @@ pub fn startup_with_log_dir(log_dir: Option<std::path::PathBuf>) {
let _ = LOG_BROADCASTER.set(broadcast_tx.clone()); let _ = LOG_BROADCASTER.set(broadcast_tx.clone());
thread::spawn(move || { thread::spawn(move || {
let mut file = log_dir.and_then(|log_dir| { let start_ts = SystemTime::now()
fs::create_dir_all(&log_dir).ok()?; .duration_since(UNIX_EPOCH)
let start_ts = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs(); .unwrap_or_default()
OpenOptions::new() .as_secs();
.create(true) let mut sequence = 0;
.append(true) let mut file = log_dir.as_ref().and_then(|log_dir| {
.open(log_dir.join(format!("log_{start_ts}.txt"))) if let Err(error) = fs::create_dir_all(log_dir) {
.ok() eprintln!(
"Unable to create Iota log directory {}: {error}",
log_dir.display()
);
return None;
}
prune_logs(log_dir);
match open_log(log_dir, start_ts, sequence) {
Ok(file) => Some(file),
Err(error) => {
eprintln!(
"Unable to open Iota log file in {}: {error}",
log_dir.display()
);
None
}
}
}); });
let mut last_drop_report = Instant::now();
for msg in rx { loop {
let msg = match rx.recv_timeout(DROPPED_LOG_REPORT_INTERVAL) {
Ok(msg) => msg,
Err(RecvTimeoutError::Timeout) => {
report_dropped_logs(&mut file, &broadcast_tx);
last_drop_report = Instant::now();
continue;
}
Err(RecvTimeoutError::Disconnected) => {
report_dropped_logs(&mut file, &broadcast_tx);
break;
}
};
let resolved_message = if let Some(key) = msg.translation_key { let resolved_message = if let Some(key) = msg.translation_key {
let args: Vec<&str> = msg.format_args.iter().map(|s| s.as_str()).collect(); let args: Vec<&str> = msg.format_args.iter().map(|s| s.as_str()).collect();
language_manager::format(&key, &args) language_manager::format(&key, &args)
@ -99,27 +175,49 @@ pub fn startup_with_log_dir(log_dir: Option<std::path::PathBuf>) {
}; };
let line = format!( let line = format!(
"{} {} {}{}", "{} level={} component={} direction={} sender=- event={} message={:?}",
fixed_box(&msg.timestamp_ms.to_string(), 13), msg.timestamp_ms,
timestamp, msg.level.as_str(),
prefix, msg.kind.as_str(),
resolved_message match msg.prefix.trim() {
">" => "in",
"<" => "out",
">>" => "internal",
_ => "local",
},
msg.event,
format!("{prefix}{resolved_message}")
); );
if let Some(file) = file.as_mut() { if let Some(file) = file.as_mut() {
let _ = writeln!(file, "{}", line); if let Err(error) = writeln!(file, "{line}") {
eprintln!("Unable to write Iota log file: {error}");
}
} }
let _ = writeln!(std::io::stderr(), "{}", line); let _ = writeln!(std::io::stderr(), "{timestamp} {line}");
let ui_message = if msg.event == "message" {
resolved_message.clone()
} else {
format!("event={} {}", msg.event, resolved_message)
};
let entry = UiLogEntry { let entry = UiLogEntry {
timestamp_ms: msg.timestamp_ms, timestamp_ms: msg.timestamp_ms,
sender: format!("{:?}", msg.kind), sender: format!("{:?}", msg.kind),
message: resolved_message, message: ui_message,
is_error: msg.is_error, is_error: msg.is_error,
}; };
let _ = broadcast_tx.send(entry); let _ = broadcast_tx.send(entry);
if last_drop_report.elapsed() >= DROPPED_LOG_REPORT_INTERVAL {
report_dropped_logs(&mut file, &broadcast_tx);
last_drop_report = Instant::now();
}
if let (Some(file), Some(log_dir)) = (file.as_mut(), log_dir.as_ref()) {
rotate_log_if_needed(file, log_dir, start_ts, &mut sequence);
}
} }
}); });
} }
@ -136,13 +234,110 @@ fn format_timestamp_inline(timestamp_ms: u128) -> String {
format!("[{:02}:{:02}:{:02}]", hours, minutes, seconds) format!("[{:02}:{:02}:{:02}]", hours, minutes, seconds)
} }
fn fixed_box(content: &str, width: usize) -> String { fn open_log(dir: &Path, start_ts: u64, sequence: u32) -> std::io::Result<std::fs::File> {
let s: String = content.chars().take(width).collect(); let mut options = OpenOptions::new();
let len = s.chars().count(); options.create(true).append(true);
if len < width { #[cfg(unix)]
format!("[{}{}]", " ".repeat(width - len), s) {
} else { use std::os::unix::fs::OpenOptionsExt;
s options.mode(0o600);
}
options.open(dir.join(format!("log_{start_ts}_{sequence}.txt")))
}
fn rotate_log_if_needed(file: &mut std::fs::File, dir: &Path, start_ts: u64, sequence: &mut u32) {
if !file
.metadata()
.is_ok_and(|meta| meta.len() >= MAX_LOG_FILE_BYTES)
{
return;
}
let next = sequence.saturating_add(1);
match open_log(dir, start_ts, next) {
Ok(new_file) => {
*file = new_file;
*sequence = next;
prune_logs(dir);
}
Err(error) => eprintln!("Unable to rotate Iota log file: {error}"),
}
}
fn prune_logs(dir: &Path) {
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(error) => {
eprintln!(
"Unable to enumerate Iota log directory {}: {error}",
dir.display()
);
return;
}
};
let mut logs = entries
.filter_map(Result::ok)
.filter(|entry| {
entry
.file_name()
.to_str()
.is_some_and(|name| name.starts_with("log_") && name.ends_with(".txt"))
})
.collect::<Vec<_>>();
logs.sort_by_key(|entry| {
entry
.metadata()
.and_then(|meta| meta.modified())
.unwrap_or(UNIX_EPOCH)
});
let remove_count = logs.len().saturating_sub(RETAINED_LOG_FILES);
for entry in logs.into_iter().take(remove_count) {
if let Err(error) = fs::remove_file(entry.path()) {
eprintln!("Unable to remove old Iota log file: {error}");
}
}
}
fn report_dropped_logs(
file: &mut Option<std::fs::File>,
broadcaster: &broadcast::Sender<UiLogEntry>,
) {
let dropped = DROPPED_LOGS.swap(0, Ordering::Relaxed);
if dropped == 0 {
return;
}
let timestamp_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
let message = format!("count={dropped}");
let line = format!(
"{timestamp_ms} level=ERROR component=general direction=internal sender=- event=logger.dropped message={message:?}"
);
if let Some(file) = file {
if let Err(error) = writeln!(file, "{line}") {
eprintln!("Unable to write Iota log file: {error}");
}
}
eprintln!("{line}");
let _ = broadcaster.send(UiLogEntry {
timestamp_ms,
sender: "General".into(),
message: format!("event=logger.dropped {message}"),
is_error: true,
});
}
fn enqueue(message: LogMessage) {
let Some(tx) = LOGGER.get() else {
return;
};
UNIQUE.store(true, Ordering::Relaxed);
match tx.try_send(message) {
Ok(()) => {}
Err(TrySendError::Full(_)) => {
DROPPED_LOGS.fetch_add(1, Ordering::Relaxed);
}
Err(TrySendError::Disconnected(_)) => eprintln!("Iota logger thread has stopped"),
} }
} }
@ -153,9 +348,7 @@ pub fn log_internal_translated(
key: &str, key: &str,
args: Vec<String>, args: Vec<String>,
) { ) {
if let Some(tx) = LOGGER.get() { enqueue(LogMessage {
UNIQUE.store(true, Ordering::Relaxed);
let _ = tx.send(LogMessage {
timestamp_ms: SystemTime::now() timestamp_ms: SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.unwrap() .unwrap()
@ -163,29 +356,60 @@ pub fn log_internal_translated(
prefix, prefix,
kind, kind,
is_error, is_error,
level: if is_error {
LogLevel::Error
} else {
LogLevel::Info
},
event: "message",
translation_key: Some(key.to_string()), translation_key: Some(key.to_string()),
format_args: args, format_args: args,
message: None, message: None,
}); });
}
} }
pub fn log_internal(kind: PrintType, prefix: String, is_error: bool, message: String) { pub fn log_internal(kind: PrintType, prefix: String, is_error: bool, message: String) {
if let Some(tx) = LOGGER.get() { log_event_internal(
UNIQUE.store(true, Ordering::Relaxed); kind,
let _ = tx.send(LogMessage { if is_error {
LogLevel::Error
} else {
LogLevel::Info
},
"message",
prefix,
message,
);
}
pub fn log_event_internal(
kind: PrintType,
level: LogLevel,
event: &'static str,
prefix: String,
message: String,
) {
enqueue(LogMessage {
timestamp_ms: SystemTime::now() timestamp_ms: SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.unwrap() .unwrap()
.as_millis(), .as_millis(),
prefix, prefix,
kind, kind,
is_error, is_error: level == LogLevel::Error,
level,
event,
translation_key: None, translation_key: None,
format_args: Vec::new(), format_args: Vec::new(),
message: Some(message), message: Some(message),
}); });
} }
#[macro_export]
macro_rules! log_event {
($kind:expr, $level:expr, $event:expr, $($arg:tt)*) => {
$crate::log_event_internal($kind, $level, $event, String::new(), format!($($arg)*))
};
} }
#[macro_export] #[macro_export]
@ -307,10 +531,15 @@ pub fn log_cv_internal(
) { ) {
let formatted = format_cv(cv); let formatted = format_cv(cv);
log_internal( log_event_internal(
print_type.unwrap_or(PrintType::General), print_type.unwrap_or(PrintType::General),
LogLevel::Debug,
if prefix.trim() == "<" {
"protocol.sent"
} else {
"protocol.received"
},
prefix.to_string(), prefix.to_string(),
false,
formatted, formatted,
); );
} }
@ -334,136 +563,103 @@ pub fn format_cv(cv: &CommunicationValue) -> String {
.map_or_else(|| "none".to_string(), |value| value.to_string()); .map_or_else(|| "none".to_string(), |value| value.to_string());
parts.push(format!("{} (id={})", comm_type, id)); parts.push(format!("{} (id={})", comm_type, id));
let version = cv if cv.is_type(CommunicationType::Relay) {
.type_map() parts.push("<opaque relay payload>".into());
.map(|type_map| type_map.version.clone()) return parts.join(": ");
.unwrap_or_else(|| Version(3, 0)); }
let formated_data = cv.data().map_or_else( if let Some(data) = cv.data() {
|| "<opaque payload>".to_string(), let type_map = cv.type_map().cloned().unwrap_or_else(TypeMap::latest);
|data| format_data_container(data.to_vec(), version), parts.push(format_data_container(data, &type_map));
); }
parts.push(format!("{}", formated_data));
parts.join(": ") parts.join(": ")
} }
fn format_data_container(data: Vec<(DataTypeId, DataValue)>, version: Version) -> String { fn format_data_container(data: &[(DataTypeId, DataValue)], type_map: &TypeMap) -> String {
let parts: Vec<String> = data data.iter()
.into_iter()
.map(|(key, value)| { .map(|(key, value)| {
let key_str = key.to_string(); let name = type_map
.data_type_name(key.0)
if is_secret_data_type(key) { .map(str::to_owned)
return format!("{}=<redacted>", key_str); .unwrap_or_else(|| key.to_string());
}
match value { match value {
DataValue::Str(s) => format!("{}=\"{}\"", key_str, abbreviate_string(&s)), DataValue::SignedNumber(value)
if matches!(
name.as_str(),
"UserId"
| "IotaId"
| "OmikronId"
| "InvitationId"
| "RelayMessageId"
| "VersionNumber"
| "Offset"
| "Amount"
) =>
{
format!("{name}={value}")
}
DataValue::UnsignedNumber(value)
if matches!(
name.as_str(),
"UserId"
| "IotaId"
| "OmikronId"
| "InvitationId"
| "RelayMessageId"
| "VersionNumber"
| "Offset"
| "Amount"
) =>
{
format!("{name}={value}")
}
DataValue::Container(inner) => { DataValue::Container(inner) => {
let inner_formatted = format_data_container(inner, version.clone()); format!("{name}={{ {} }}", format_data_container(inner, type_map))
format!("{}={{ {} }}", key_str, inner_formatted)
} }
DataValue::Array(values) => format!("{name}=<array:{}>", values.len()),
DataValue::Array(arr) => { DataValue::Str(_) => format!("{name}=<string>"),
let arr_formatted = format_array(arr, version.clone()); DataValue::Bytes(_) => format!("{name}=<bytes>"),
format!("{}=[{}]", key_str, arr_formatted) DataValue::Bool(_) | DataValue::BoolTrue | DataValue::BoolFalse => {
format!("{name}=<bool>")
} }
DataValue::SignedNumber(_) | DataValue::UnsignedNumber(_) => {
DataValue::Bool(b) => format!("{}={}", key_str, b), format!("{name}=<number>")
}
DataValue::BoolTrue => format!("{}=true", key_str), _ => format!("{name}=<value>"),
DataValue::BoolFalse => format!("{}=false", key_str),
DataValue::SignedNumber(num) => format!("{}={}", key_str, num),
_ => "".to_string(),
} }
}) })
.collect(); .collect::<Vec<_>>()
.join(", ")
parts.join(", ")
}
fn is_secret_data_type(key: DataTypeId) -> bool {
matches!(
TypeMap::latest().data_type_name(key.0),
Some("InvitationToken" | "InvitationPassword" | "ResetToken" | "RegisterId" | "NewToken")
)
}
fn format_array(arr: Vec<DataValue>, version: Version) -> String {
let parts: Vec<String> = arr
.into_iter()
.map(|value| match value {
DataValue::Str(s) => format!("\"{}\"", abbreviate_string(&s)),
DataValue::Container(inner) => {
let inner_formatted = format_data_container(inner, version.clone());
format!("{{ {} }}", inner_formatted)
}
DataValue::Array(inner_arr) => {
let formatted = format_array(inner_arr, version.clone());
format!("[{}]", formatted)
}
DataValue::Bool(b) => b.to_string(),
DataValue::BoolTrue => "true".to_string(),
DataValue::BoolFalse => "false".to_string(),
DataValue::SignedNumber(num) => num.to_string(),
_ => String::new(),
})
.collect();
parts.join(", ")
}
fn abbreviate_string(value: &str) -> String {
const EDGE_LENGTH: usize = 4;
let chars: Vec<char> = value.chars().collect();
if chars.len() <= EDGE_LENGTH * 2 {
return value.to_string();
}
let prefix: String = chars.iter().take(EDGE_LENGTH).collect();
let suffix: String = chars.iter().rev().take(EDGE_LENGTH).rev().collect();
format!("{prefix}...{suffix}")
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{abbreviate_string, format_cv}; use super::format_cv;
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue, TypeMap}; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
#[test] #[test]
fn abbreviates_only_strings_longer_than_eight_characters() { fn protocol_values_are_metadata_only() {
assert_eq!(abbreviate_string("12345678"), "12345678"); let value = CommunicationValue::new(CommunicationType::Success)
assert_eq!(abbreviate_string("123456789"), "1234...6789");
assert_eq!(abbreviate_string("YWJjZGVmZ2hpag=="), "YWJj...ag==");
}
#[test]
fn redacts_invitation_and_account_credentials() {
let reset_token = DataType::ResetToken.try_to_id(&TypeMap::latest()).unwrap();
let value = CommunicationValue::new(CommunicationType::RedeemUserInvitation)
.add_typed_default(DataType::InvitationToken, DataValue::Str("short123".into()))
.add_typed_default( .add_typed_default(
DataType::Invitations, DataType::SessionToken,
DataValue::Array(vec![DataValue::Container(vec![( DataValue::Str("session-secret-value".into()),
reset_token, )
DataValue::Str("reset-secret".into()), .add_typed_default(
)])]), DataType::CallToken,
); DataValue::Str("livekit-secret-token".into()),
)
.add_typed_default(DataType::Username, DataValue::Str("alice".into()))
.add_typed_default(DataType::UserId, DataValue::SignedNumber(172));
let formatted = format_cv(&value); let formatted = format_cv(&value);
assert!(!formatted.contains("short123")); for secret in ["session-secret-value", "livekit-secret-token", "alice"] {
assert!(!formatted.contains("reset-secret")); assert!(!formatted.contains(secret));
assert_eq!(formatted.matches("<redacted>").count(), 2); }
assert!(formatted.contains("SessionToken=<string>"));
assert!(formatted.contains("UserId=172"));
assert!(
format_cv(&CommunicationValue::new(CommunicationType::Relay))
.contains("<opaque relay payload>")
);
} }
} }

View file

@ -30,6 +30,26 @@ const DEFAULT_ACTIVATION_TIMEOUT_SECONDS: u64 = 60;
const DEFAULT_ACTIVATION_RETRY_MILLISECONDS: u64 = 250; const DEFAULT_ACTIVATION_RETRY_MILLISECONDS: u64 = 250;
const REQUIRED_HOST_ARTIFACTS: &str = include_str!("../artifacts.tsv"); const REQUIRED_HOST_ARTIFACTS: &str = include_str!("../artifacts.tsv");
#[derive(Clone, Copy, Debug)]
pub enum UpdatePhase {
CandidateEvaluated,
ArtifactsStaged,
ActivationStarted,
DaemonRestarted,
ActivationCommitted,
RollbackStarted,
RollbackCompleted,
}
pub trait UpdateObserver: Send + Sync {
fn event(&self, phase: UpdatePhase, product_version: Option<&str>);
}
pub struct NoopUpdateObserver;
impl UpdateObserver for NoopUpdateObserver {
fn event(&self, _phase: UpdatePhase, _product_version: Option<&str>) {}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ActivationPolicy { pub struct ActivationPolicy {
pub timeout: Duration, pub timeout: Duration,
@ -176,16 +196,21 @@ pub async fn check_update() -> Result<bool> {
} }
pub async fn apply_update() -> Result<bool> { pub async fn apply_update() -> Result<bool> {
apply_update_observed(&NoopUpdateObserver).await
}
pub async fn apply_update_observed(observer: &dyn UpdateObserver) -> Result<bool> {
let paths = iota_paths::IotaPaths::resolve(iota_paths::Scope::System) let paths = iota_paths::IotaPaths::resolve(iota_paths::Scope::System)
.map_err(|error| anyhow::anyhow!(error))?; .map_err(|error| anyhow::anyhow!(error))?;
let policy = UpdatePolicy::from_environment()?; let policy = UpdatePolicy::from_environment()?;
apply_update_with(&paths, &SystemdSupervisor, &policy).await apply_update_with_observer(&paths, &SystemdSupervisor, &policy, observer).await
} }
async fn apply_update_with( async fn apply_update_with_observer(
paths: &iota_paths::IotaPaths, paths: &iota_paths::IotaPaths,
supervisor: &impl DaemonSupervisor, supervisor: &impl DaemonSupervisor,
policy: &UpdatePolicy, policy: &UpdatePolicy,
observer: &dyn UpdateObserver,
) -> Result<bool> { ) -> Result<bool> {
let transaction = UpdateTransaction::from_paths(paths)?; let transaction = UpdateTransaction::from_paths(paths)?;
let _lock = transaction.acquire()?; let _lock = transaction.acquire()?;
@ -209,6 +234,10 @@ async fn apply_update_with(
&policy.signing_key_id, &policy.signing_key_id,
Utc::now(), Utc::now(),
)?; )?;
observer.event(
UpdatePhase::CandidateEvaluated,
Some(&release.manifest.product_version),
);
if decision == CandidateDecision::NoUpdate { if decision == CandidateDecision::NoUpdate {
return Ok(false); return Ok(false);
} }
@ -236,6 +265,10 @@ async fn apply_update_with(
serde_json::to_vec_pretty(&release.manifest)?, serde_json::to_vec_pretty(&release.manifest)?,
) )
.context("write staged release manifest")?; .context("write staged release manifest")?;
observer.event(
UpdatePhase::ArtifactsStaged,
Some(&release.manifest.product_version),
);
let daemon_was_active = supervisor.is_active()?; let daemon_was_active = supervisor.is_active()?;
begin_activation_state( begin_activation_state(
paths, paths,
@ -244,6 +277,10 @@ async fn apply_update_with(
&release.manifest, &release.manifest,
daemon_was_active, daemon_was_active,
)?; )?;
observer.event(
UpdatePhase::ActivationStarted,
Some(&release.manifest.product_version),
);
let activation = match transaction.activate(&release.manifest.product_version) { let activation = match transaction.activate(&release.manifest.product_version) {
Ok(activation) => activation, Ok(activation) => activation,
Err(error) => { Err(error) => {
@ -263,13 +300,25 @@ async fn apply_update_with(
&policy.activation, &policy.activation,
) )
.await?; .await?;
observer.event(
UpdatePhase::DaemonRestarted,
Some(&release.manifest.product_version),
);
} }
commit_update_state(paths, &mut state, &release.manifest)?; commit_update_state(paths, &mut state, &release.manifest)?;
observer.event(
UpdatePhase::ActivationCommitted,
Some(&release.manifest.product_version),
);
Ok::<_, anyhow::Error>(()) Ok::<_, anyhow::Error>(())
} }
.await; .await;
if let Err(update_error) = activation_result { if let Err(update_error) = activation_result {
observer.event(
UpdatePhase::RollbackStarted,
Some(&release.manifest.product_version),
);
restore_previous_release( restore_previous_release(
paths, paths,
&transaction, &transaction,
@ -279,6 +328,10 @@ async fn apply_update_with(
&policy.activation, &policy.activation,
) )
.await?; .await?;
observer.event(
UpdatePhase::RollbackCompleted,
Some(&release.manifest.product_version),
);
record_failed_release(paths, &mut state, &release.manifest)?; record_failed_release(paths, &mut state, &release.manifest)?;
return Err( return Err(
update_error.context("candidate release failed runtime activation and was rolled back") update_error.context("candidate release failed runtime activation and was rolled back")

View file

@ -6,7 +6,18 @@ async fn main() -> Result<()> {
match command.as_str() { match command.as_str() {
"check" => println!("{}", iota_updater::check_update().await?), "check" => println!("{}", iota_updater::check_update().await?),
"status" => println!("updater ready"), "status" => println!("updater ready"),
"apply" => println!("{}", iota_updater::apply_update().await?), "apply" => {
struct StderrObserver;
impl iota_updater::UpdateObserver for StderrObserver {
fn event(&self, phase: iota_updater::UpdatePhase, product_version: Option<&str>) {
eprintln!("update.phase phase={phase:?} product_version={product_version:?}");
}
}
println!(
"{}",
iota_updater::apply_update_observed(&StderrObserver).await?
);
}
"rollback" => { "rollback" => {
let version = std::env::args() let version = std::env::args()
.nth(2) .nth(2)

View file

@ -1,6 +1,6 @@
use base64::{Engine as _, engine::general_purpose::STANDARD}; use base64::{Engine as _, engine::general_purpose::STANDARD};
use dashmap::{DashMap, DashSet}; use dashmap::{DashMap, DashSet};
use iota_logger::{log, log_cv_in, log_cv_out, log_t}; use iota_logger::{LogLevel, PrintType, log, log_cv_in, log_cv_out, log_event, log_t};
use iota_state::AppState; use iota_state::AppState;
use iota_storage::util::config_util::{CONFIG, modify_config}; use iota_storage::util::config_util::{CONFIG, modify_config};
use iota_storage::util::relay_replay; use iota_storage::util::relay_replay;
@ -3103,6 +3103,15 @@ impl OmikronConnection {
let sender_guard = self.sender.read().await; let sender_guard = self.sender.read().await;
if let Some(sender) = sender_guard.as_ref() { if let Some(sender) = sender_guard.as_ref() {
if !sender.is_open() { if !sender.is_open() {
log_event!(
PrintType::Omikron,
LogLevel::Warn,
"omikron.send_failed",
"connection_id={} message_id={:?} message_type={} reason=connection_closed",
self.connection_id,
cv.id(),
cv.get_type_name().unwrap_or("unknown")
);
drop(sender_guard); drop(sender_guard);
if let Some(sender) = self.sender.write().await.take() { if let Some(sender) = self.sender.write().await.take() {
sender.close().await; sender.close().await;
@ -3121,6 +3130,16 @@ impl OmikronConnection {
log_cv_out!(cv); log_cv_out!(cv);
if let Err(e) = sender_clone.send(cv).await { if let Err(e) = sender_clone.send(cv).await {
log_event!(
PrintType::Omikron,
LogLevel::Error,
"omikron.send_failed",
"connection_id={} message_id={:?} message_type={} error={}",
self.connection_id,
cv.id(),
cv.get_type_name().unwrap_or("unknown"),
e
);
self.fail_all_waiting_tasks(format!( self.fail_all_waiting_tasks(format!(
"Send failed: {} (connection_id={})", "Send failed: {} (connection_id={})",
e, self.connection_id e, self.connection_id
@ -3131,6 +3150,15 @@ impl OmikronConnection {
Ok(()) Ok(())
} else { } else {
log_event!(
PrintType::Omikron,
LogLevel::Debug,
"omikron.send_skipped",
"connection_id={} message_id={:?} message_type={} reason=not_connected",
self.connection_id,
cv.id(),
cv.get_type_name().unwrap_or("unknown")
);
Err("not connected".to_string()) Err("not connected".to_string())
} }
} }