Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

structured logging for node + WebAssembly bindings #1235

Merged
merged 2 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ updates:
schedule:
interval: "weekly"
# Maintain dependencies for yarn
- package-ecosystem: "yarn"
- package-ecosystem: "npm"
directory: "/bindings_wasm"
schedule:
interval: "weekly"
# Maintain dependencies for yarn
- package-ecosystem: "yarn"
- package-ecosystem: "npm"
directory: "/bindings_node"
schedule:
interval: "weekly"
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ trait-variant = "0.1.2"
url = "2.5.0"
zeroize = "1.8"
bincode = "1.3"
console_error_panic_hook = "0.1"

# Internal Crate Dependencies
xmtp_cryptography = { path = "xmtp_cryptography" }
Expand Down
7 changes: 5 additions & 2 deletions bindings_ffi/src/mls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ pub struct FfiXmtpClient {
#[uniffi::export(async_runtime = "tokio")]
impl FfiXmtpClient {
pub fn inbox_id(&self) -> InboxId {
self.inner_client.inbox_id()
self.inner_client.inbox_id().to_string()
}

pub fn conversations(&self) -> Arc<FfiConversations> {
Expand Down Expand Up @@ -1258,7 +1258,10 @@ impl FfiConversation {
&self,
inbox_ids: Vec<String>,
) -> Result<(), GenericError> {
self.inner.remove_members_by_inbox_id(&inbox_ids).await?;
let ids = inbox_ids.iter().map(AsRef::as_ref).collect::<Vec<&str>>();
self.inner
.remove_members_by_inbox_id(ids.as_slice())
.await?;
Ok(())
}

Expand Down
2 changes: 1 addition & 1 deletion bindings_node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ napi = { version = "2.12.2", default-features = false, features = [
napi-derive = "2.12.2"
prost.workspace = true
tokio = { workspace = true, features = ["sync"] }
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json", "chrono"] }
tracing.workspace = true
xmtp_api_grpc = { path = "../xmtp_api_grpc" }
xmtp_cryptography = { path = "../xmtp_cryptography" }
Expand Down
94 changes: 77 additions & 17 deletions bindings_node/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ use napi::bindgen_prelude::{Error, Result, Uint8Array};
use napi_derive::napi;
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::{Arc, Once};
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing_subscriber::{filter::EnvFilter, fmt, prelude::*};
use tracing_subscriber::{fmt, prelude::*};
pub use xmtp_api_grpc::grpc_api_helper::Client as TonicApiClient;
use xmtp_cryptography::signature::ed25519_public_key_to_address;
use xmtp_id::associations::builder::SignatureRequest;
Expand All @@ -20,7 +21,7 @@ use xmtp_mls::Client as MlsClient;
use xmtp_proto::xmtp::mls::message_contents::DeviceSyncKind;

pub type RustXmtpClient = MlsClient<TonicApiClient>;
static LOGGER_INIT: Once = Once::new();
static LOGGER_INIT: std::sync::OnceLock<Result<()>> = std::sync::OnceLock::new();

#[napi]
pub struct Client {
Expand All @@ -39,6 +40,76 @@ impl Client {
}
}

#[napi(string_enum)]
#[derive(Debug)]
#[allow(non_camel_case_types)]
pub enum Level {
off,
error,
warn,
info,
debug,
trace,
}

impl std::fmt::Display for Level {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use Level::*;
let s = match self {
off => "off",
error => "error",
warn => "warn",
info => "info",
debug => "debug",
trace => "trace",
};
write!(f, "{}", s)
}
}

/// Specify options for the logger
#[napi(object)]
#[derive(Default)]
pub struct LogOptions {
/// enable structured JSON logging to stdout.Useful for third-party log viewers
/// an option so that it does not require being specified in js object.
pub structured: Option<bool>,
/// Filter logs by level
pub level: Option<Level>,
}

fn init_logging(options: LogOptions) -> Result<()> {
LOGGER_INIT
.get_or_init(|| {
let filter = if let Some(f) = options.level {
tracing_subscriber::filter::LevelFilter::from_str(&f.to_string())
} else {
Ok(tracing_subscriber::filter::LevelFilter::INFO)
}
.map_err(ErrorWrapper::from)?;

if options.structured.unwrap_or_default() {
let fmt = tracing_subscriber::fmt::layer()
.json()
.flatten_event(true)
.with_level(true)
.with_timer(tracing_subscriber::fmt::time::ChronoLocal::rfc_3339())
.with_target(true);

tracing_subscriber::registry().with(filter).with(fmt).init();
} else {
tracing_subscriber::registry()
.with(fmt::layer())
.with(filter)
.init();
}
Ok(())
})
.clone()
.map_err(ErrorWrapper::from)?;
Ok(())
}

/**
* Create a client
*
Expand All @@ -56,20 +127,9 @@ pub async fn create_client(
account_address: String,
encryption_key: Option<Uint8Array>,
history_sync_url: Option<String>,
#[napi(ts_arg_type = "\"debug\" | \"info\" | \"warn\" | \"error\" | \"off\" | undefined | null")]
env_filter: Option<String>,
log_options: Option<LogOptions>,
) -> Result<Client> {
LOGGER_INIT.call_once(|| {
let filter = EnvFilter::builder()
.with_regex(false)
.with_default_directive(tracing::metadata::LevelFilter::INFO.into())
.parse_lossy(env_filter.unwrap_or_default());

tracing_subscriber::registry()
.with(fmt::layer())
.with(filter)
.init();
});
init_logging(log_options.unwrap_or_default())?;
let api_client = TonicApiClient::create(host.clone(), is_secure)
.await
.map_err(|_| Error::from_reason("Error creating Tonic API client"))?;
Expand Down Expand Up @@ -127,7 +187,7 @@ pub async fn create_client(
impl Client {
#[napi]
pub fn inbox_id(&self) -> String {
self.inner_client.inbox_id()
self.inner_client.inbox_id().to_string()
}

#[napi]
Expand Down
8 changes: 7 additions & 1 deletion bindings_node/src/conversation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,13 @@ impl Conversation {
);

group
.remove_members_by_inbox_id(&inbox_ids)
.remove_members_by_inbox_id(
inbox_ids
.iter()
.map(AsRef::as_ref)
.collect::<Vec<&str>>()
.as_slice(),
)
.await
.map_err(ErrorWrapper::from)?;

Expand Down
1 change: 1 addition & 0 deletions bindings_node/test/Client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,4 +241,5 @@ describe('Client', () => {
user2.account.address.toLowerCase(),
])
})
it('should create client with structured logging', async () => {})
})
2 changes: 1 addition & 1 deletion bindings_node/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const createClient = async (user: User) => {
user.account.address,
undefined,
undefined,
'error'
{ level: 'info' }
)
}

Expand Down
4 changes: 4 additions & 0 deletions bindings_wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ xmtp_cryptography = { path = "../xmtp_cryptography" }
xmtp_id = { path = "../xmtp_id" }
xmtp_mls = { path = "../xmtp_mls", features = ["test-utils", "http-api"] }
xmtp_proto = { path = "../xmtp_proto", features = ["proto_full"] }
tracing-web = "0.1"
tracing.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter", "json"] }
console_error_panic_hook.workspace = true

[dev-dependencies]
wasm-bindgen-test.workspace = true
Expand Down
77 changes: 75 additions & 2 deletions bindings_wasm/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use js_sys::Uint8Array;
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{filter, fmt::format::Pretty};
use wasm_bindgen::prelude::{wasm_bindgen, JsError};
use wasm_bindgen::JsValue;
use xmtp_api_http::XmtpHttpApiClient;
Expand Down Expand Up @@ -36,6 +40,73 @@ impl Client {
}
}

static LOGGER_INIT: std::sync::OnceLock<Result<(), filter::LevelParseError>> =
std::sync::OnceLock::new();

#[wasm_bindgen]
#[derive(Copy, Clone, Debug)]
pub enum Level {
Off = "off",
Error = "error",
Warn = "warn",
Info = "info",
Debug = "debug",
Trace = "trace",
}

/// Specify options for the logger
#[derive(Default)]
#[wasm_bindgen(getter_with_clone)]
pub struct LogOptions {
/// enable structured JSON logging to stdout.Useful for third-party log viewers
pub structured: bool,
/// enable performance metrics for libxmtp in the `performance` tab
pub performance: bool,
/// filter for logs
pub level: Option<Level>,
}

fn init_logging(options: LogOptions) -> Result<(), JsError> {
LOGGER_INIT
.get_or_init(|| {
console_error_panic_hook::set_once();
let filter = if let Some(f) = options.level {
tracing_subscriber::filter::LevelFilter::from_str(f.to_str())
} else {
Ok(tracing_subscriber::filter::LevelFilter::INFO)
}?;

if options.structured {
let fmt = tracing_subscriber::fmt::layer()
.json()
.flatten_event(true)
.with_level(true)
.without_time() // need to test whether this would break browsers
.with_target(true);

tracing_subscriber::registry().with(filter).with(fmt).init();
} else {
let fmt = tracing_subscriber::fmt::layer()
.with_ansi(false) // not supported by all browsers
.without_time() // std::time break things, but chrono might work
.with_writer(tracing_web::MakeWebConsoleWriter::new());

let subscriber = tracing_subscriber::registry().with(fmt).with(filter);

if options.performance {
subscriber
.with(tracing_web::performance_layer().with_details_from_fields(Pretty::default()))
.init();
} else {
subscriber.init();
}
}
Ok(())
})
.clone()?;
Ok(())
}

#[wasm_bindgen(js_name = createClient)]
pub async fn create_client(
host: String,
Expand All @@ -44,8 +115,10 @@ pub async fn create_client(
db_path: String,
encryption_key: Option<Uint8Array>,
history_sync_url: Option<String>,
log_options: Option<LogOptions>,
) -> Result<Client, JsError> {
xmtp_mls::utils::wasm::init().await;
init_logging(log_options.unwrap_or_default())?;
xmtp_mls::storage::init_sqlite().await;
let api_client = XmtpHttpApiClient::new(host.clone()).unwrap();

let storage_option = StorageOption::Persistent(db_path);
Expand Down Expand Up @@ -105,7 +178,7 @@ impl Client {

#[wasm_bindgen(getter, js_name = inboxId)]
pub fn inbox_id(&self) -> String {
self.inner_client.inbox_id()
self.inner_client.inbox_id().to_string()
}

#[wasm_bindgen(getter, js_name = isRegistered)]
Expand Down
3 changes: 2 additions & 1 deletion bindings_wasm/src/conversation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,8 +353,9 @@ impl Conversation {
pub async fn remove_members_by_inbox_id(&self, inbox_ids: Vec<String>) -> Result<(), JsError> {
let group = self.to_mls_group();

let ids = inbox_ids.iter().map(AsRef::as_ref).collect::<Vec<&str>>();
group
.remove_members_by_inbox_id(&inbox_ids)
.remove_members_by_inbox_id(ids.as_slice())
.await
.map_err(|e| JsError::new(&format!("{e}")))?;

Expand Down
Loading
Loading