blob: 5e3d0d13613ba48a10678bac6dc32c865c8e1e00 [file]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::target::WeakTarget,
anyhow::{anyhow, bail, Context, Result},
diagnostics_data::{LogsData, Timestamp},
ffx_config::get,
ffx_log_data::{EventType, LogData, LogEntry},
fidl::endpoints::create_proxy,
fidl::endpoints::ServerEnd,
fidl_fuchsia_developer_remotecontrol::{
ArchiveIteratorMarker, BridgeStreamParameters, RemoteDiagnosticsBridgeMarker,
},
futures::TryFutureExt,
selectors::parse_selector,
std::future::Future,
std::sync::Arc,
std::time::SystemTime,
streamer::GenericDiagnosticsStreamer,
};
pub mod streamer;
const BRIDGE_SELECTOR: &str =
"core/remote-diagnostics-bridge:out:fuchsia.developer.remotecontrol.RemoteDiagnosticsBridge";
const ENABLED_CONFIG: &str = "proactive_log.enabled";
fn get_timestamp() -> Result<Timestamp> {
Ok(Timestamp::from(
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.context("system time before Unix epoch")?
.as_nanos(),
))
}
fn write_logs_to_file<T: GenericDiagnosticsStreamer + 'static + Send + ?Sized>(
streamer: Arc<T>,
) -> Result<(ServerEnd<ArchiveIteratorMarker>, impl Future<Output = Result<()>>)> {
let (proxy, server) =
create_proxy::<ArchiveIteratorMarker>().context("failed to create endpoints")?;
let listener_fut = async move {
let mut skip_timestamp = streamer.read_most_recent_timestamp().await?;
streamer
.append_logs(vec![LogEntry {
data: LogData::FfxEvent(EventType::LoggingStarted),
version: 1,
timestamp: get_timestamp()?,
}])
.await?;
loop {
let result = proxy.get_next().await.context("waiting for new log")?;
match result {
Ok(logs) => {
if logs.is_empty() {
break;
}
let ts = Timestamp::from(get_timestamp()?);
let log_data = logs
.iter()
.map(|l| l.data.clone())
.filter(|l| l.is_some())
.map(|l| l.unwrap())
.map(|s| {
let data: LogData = match serde_json::from_str::<LogsData>(&s) {
Ok(data) => LogData::TargetLog(data),
Err(_) => LogData::MalformedTargetLog(s.clone()),
};
LogEntry { data: data, timestamp: ts, version: 1 }
})
.filter(|log| {
// TODO(jwing): use a monotonic ID instead of timestamp
// once fxbug.dev/61795 is resolved.
if let Some(ts) = skip_timestamp {
match &log.data {
LogData::TargetLog(log_data) => {
if log_data.metadata.timestamp > ts {
skip_timestamp = None;
true
} else {
false
}
}
_ => true,
}
} else {
true
}
})
.collect();
streamer.append_logs(log_data).await?;
}
Err(e) => {
// TODO(jwing): consider exiting if we see a large number of successive errors
// from the diagnostics bridge.
log::warn!("got an error from diagnostics bridge {:?}", e);
}
}
}
let resp: Result<()> = Ok(());
resp
};
return Ok((server, listener_fut));
}
pub struct Logger {
target: WeakTarget,
enabled: Option<bool>,
streamer: Option<Arc<dyn GenericDiagnosticsStreamer + Send + Sync>>,
}
impl Logger {
pub fn new(target: WeakTarget) -> Self {
return Self { target: target, enabled: None, streamer: None };
}
#[cfg(test)]
pub fn new_with_streamer_and_config(
target: WeakTarget,
streamer: impl GenericDiagnosticsStreamer + 'static + Send + Sync,
enabled: bool,
) -> Self {
return Self { target, enabled: Some(enabled), streamer: Some(Arc::new(streamer)) };
}
pub fn start(self) -> impl Future<Output = Result<(), String>> + Send {
async move {
let enabled = match self.enabled {
Some(e) => e,
None => get(ENABLED_CONFIG).await.unwrap_or(false),
};
if !enabled {
log::info!("proactive logger disabled. exiting...");
return Ok(());
}
self.run_logger()
.map_err(|e| {
log::error!("error running logger: {:?}", e);
format!("{}", e)
})
.await
}
}
async fn run_logger(&self) -> Result<()> {
let target = self.target.upgrade().context("lost parent Arc")?;
log::info!("starting logger for {}", target.nodename_str().await);
let remote_proxy = target.rcs().await.context("failed to get RCS")?.proxy;
let nodename = target.nodename_str().await;
let (log_proxy, log_server_end) = create_proxy::<RemoteDiagnosticsBridgeMarker>()?;
let selector = parse_selector(BRIDGE_SELECTOR).unwrap();
match remote_proxy.connect(selector, log_server_end.into_channel()).await? {
Ok(_) => {}
Err(e) => {
log::info!("attempt to connect to logger for {} failed. {:?}", nodename, e);
bail!("{:?}", e);
}
};
let streamer = if self.streamer.is_some() {
self.streamer.as_ref().unwrap().clone()
} else {
target.stream_info()
};
let nodename = target.nodename_str().await;
let boot_timestamp = target
.boot_timestamp_nanos()
.await
.with_context(|| format!("no boot timestamp for target {:?}", &nodename))?;
streamer.setup_stream(nodename, boot_timestamp).await?;
// Garbage collect old sessions before kicking off the log stream.
streamer.clean_sessions_for_target().await?;
let (listener_client, listener_fut) = write_logs_to_file(streamer.clone())?;
let params = BridgeStreamParameters {
stream_mode: Some(fidl_fuchsia_diagnostics::StreamMode::SnapshotThenSubscribe),
data_type: Some(fidl_fuchsia_diagnostics::DataType::Logs),
..BridgeStreamParameters::EMPTY
};
let _ = log_proxy
.stream_diagnostics(params, listener_client)
.await?
.map_err(|s| anyhow!("failure setting up diagnostics stream: {:?}", s))?;
let _: () = listener_fut.await?;
Ok(())
}
}
#[cfg(test)]
mod test {
use {
super::*,
crate::target::{RcsConnection, Target},
crate::NodeId,
async_std::sync::Mutex,
async_trait::async_trait,
diagnostics_data::{LogsField, Severity},
diagnostics_hierarchy::{DiagnosticsHierarchy, Property},
fidl::endpoints::RequestStream,
fidl_fuchsia_developer_remotecontrol::{
ArchiveIteratorEntry, ArchiveIteratorError, ArchiveIteratorMarker,
ArchiveIteratorRequest, IdentifyHostResponse, RemoteControlMarker, RemoteControlProxy,
RemoteControlRequest, RemoteDiagnosticsBridgeRequest,
RemoteDiagnosticsBridgeRequestStream, ServiceMatch,
},
fidl_fuchsia_diagnostics::DataType,
futures::TryStreamExt,
streamer::SessionStream,
};
const NODENAME: &str = "nodename-foo";
const BOOT_TIME: u64 = 98765432123;
#[derive(Default)]
struct FakeDiagnosticsStreamerInner {
nodename: String,
boot_time: u64,
log_buf: Arc<Mutex<Vec<LogEntry>>>,
most_recent_ts: u64,
expect_setup: bool,
cleaned_sessions: bool,
}
struct FakeDiagnosticsStreamer {
// This struct has to be Send + Sync to be compatible with the Logger implementation.
inner: Mutex<FakeDiagnosticsStreamerInner>,
}
impl FakeDiagnosticsStreamer {
fn new(most_recent_ts: u64, log_buf: Arc<Mutex<Vec<LogEntry>>>) -> Self {
Self {
inner: Mutex::new(FakeDiagnosticsStreamerInner {
most_recent_ts,
log_buf,
..FakeDiagnosticsStreamerInner::default()
}),
}
}
async fn expect_setup(&self, nodename: &str, boot_time: u64) {
let mut inner = self.inner.lock().await;
inner.expect_setup = true;
inner.nodename = nodename.to_string();
inner.boot_time = boot_time;
}
async fn assert_cleaned_sessions(&self) {
let inner = self.inner.lock().await;
assert!(inner.cleaned_sessions);
}
}
#[async_trait]
impl GenericDiagnosticsStreamer for FakeDiagnosticsStreamer {
async fn setup_stream(
&self,
target_nodename: String,
target_boot_time_nanos: u64,
) -> Result<()> {
let inner = self.inner.lock().await;
if !inner.expect_setup {
panic!("unexpected call to setup_stream");
}
assert_eq!(inner.nodename, target_nodename);
assert_eq!(inner.boot_time, target_boot_time_nanos);
Ok(())
}
async fn append_logs(&self, entries: Vec<LogEntry>) -> Result<()> {
self.assert_cleaned_sessions().await;
let inner = self.inner.lock().await;
inner.log_buf.lock().await.extend(entries);
Ok(())
}
async fn read_most_recent_timestamp(&self) -> Result<Option<Timestamp>> {
let inner = self.inner.lock().await;
Ok(Some(Timestamp::from(inner.most_recent_ts)))
}
async fn clean_sessions_for_target(&self) -> Result<()> {
let mut inner = self.inner.lock().await;
inner.cleaned_sessions = true;
Ok(())
}
async fn stream_entries(
&self,
_stream_mode: fidl_fuchsia_developer_bridge::StreamMode,
) -> Result<SessionStream> {
panic!("unexpected stream_entries call");
}
}
async fn verify_logged(got: Arc<Mutex<Vec<LogEntry>>>, expected: Vec<LogEntry>) {
let logs = got.lock().await;
assert_eq!(
logs.len(),
expected.len(),
"length mismatch: \ngot: {:?}\nexpected: {:?}",
logs,
expected
);
for (got, expected) in logs.iter().zip(expected.iter()) {
assert_eq!(
got.data, expected.data,
"mismatched data. got: {:?}\nexpected: {:?}",
got.data, expected.data
);
assert_eq!(
got.version, expected.version,
"mismatched version. got: {:?}\nexpected: {:?}",
got.version, expected.version
);
}
}
struct FakeArchiveIteratorResponse {
values: Vec<String>,
iterator_error: Option<ArchiveIteratorError>,
}
fn setup_fake_archive_iterator(
server_end: ServerEnd<ArchiveIteratorMarker>,
responses: Arc<Vec<FakeArchiveIteratorResponse>>,
) -> Result<()> {
let mut stream = server_end.into_stream()?;
fuchsia_async::Task::spawn(async move {
let mut iter = responses.iter();
while let Ok(Some(req)) = stream.try_next().await {
match req {
ArchiveIteratorRequest::GetNext { responder } => {
let next = iter.next();
match next {
Some(FakeArchiveIteratorResponse { values, iterator_error }) => {
if let Some(err) = iterator_error {
responder.send(&mut Err(*err)).unwrap();
} else {
responder
.send(&mut Ok(values
.iter()
.map(|s| ArchiveIteratorEntry {
data: Some(s.clone()),
truncated_chars: Some(0),
..ArchiveIteratorEntry::EMPTY
})
.collect()))
.unwrap()
}
}
None => responder.send(&mut Ok(vec![])).unwrap(),
}
}
}
}
})
.detach();
Ok(())
}
fn setup_fake_archive_accessor(
chan: fidl::Channel,
responses: Arc<Vec<FakeArchiveIteratorResponse>>,
) -> Result<()> {
let mut stream = RemoteDiagnosticsBridgeRequestStream::from_channel(
fidl::AsyncChannel::from_channel(chan)?,
);
fuchsia_async::Task::spawn(async move {
while let Ok(Some(req)) = stream.try_next().await {
match req {
RemoteDiagnosticsBridgeRequest::StreamDiagnostics {
responder,
iterator,
parameters,
} => {
assert_eq!(parameters.data_type.unwrap(), DataType::Logs);
assert_eq!(
parameters.stream_mode.unwrap(),
fidl_fuchsia_diagnostics::StreamMode::SnapshotThenSubscribe
);
setup_fake_archive_iterator(iterator, responses.clone()).unwrap();
responder.send(&mut Ok(())).unwrap();
}
_ => assert!(false),
}
}
})
.detach();
Ok(())
}
fn setup_fake_remote_control_service(
responses: Arc<Vec<FakeArchiveIteratorResponse>>,
) -> RemoteControlProxy {
let (proxy, mut stream) =
fidl::endpoints::create_proxy_and_stream::<RemoteControlMarker>().unwrap();
fuchsia_async::Task::spawn(async move {
while let Ok(Some(req)) = stream.try_next().await {
match req {
RemoteControlRequest::Connect { selector: _, service_chan, responder } => {
setup_fake_archive_accessor(service_chan, responses.clone()).unwrap();
responder
.send(&mut Ok(ServiceMatch {
moniker: vec![],
subdir: String::default(),
service: String::default(),
}))
.unwrap();
}
RemoteControlRequest::IdentifyHost { responder } => {
responder
.send(&mut Ok(IdentifyHostResponse {
nodename: Some(NODENAME.to_string()),
addresses: None,
boot_timestamp_nanos: Some(BOOT_TIME),
..IdentifyHostResponse::EMPTY
}))
.context("sending testing response")
.unwrap();
}
r => assert!(false, "{:?}", r),
}
}
})
.detach();
proxy
}
fn logging_started_entry() -> LogEntry {
LogEntry {
data: LogData::FfxEvent(EventType::LoggingStarted),
timestamp: Timestamp::from(0u64),
version: 1,
}
}
fn malformed_log(s: &str) -> LogEntry {
LogEntry {
data: LogData::MalformedTargetLog(s.to_string()),
timestamp: Timestamp::from(0u64),
version: 1,
}
}
fn valid_log(data: LogsData) -> LogEntry {
LogEntry { data: LogData::TargetLog(data), timestamp: Timestamp::from(0u64), version: 1 }
}
fn target_log(timestamp: u64, msg: &str) -> LogsData {
let hierarchy = DiagnosticsHierarchy::new(
"root",
vec![Property::String(LogsField::Msg, msg.to_string())],
vec![],
);
LogsData::for_logs(
String::from("test/moniker"),
Some(hierarchy),
timestamp,
String::from("fake-url"),
Severity::Error,
1,
vec![],
)
}
async fn make_default_target(expected_logs: Vec<FakeArchiveIteratorResponse>) -> Target {
let ascendd = Arc::new(crate::onet::create_ascendd().await.unwrap());
let conn = RcsConnection::new_with_proxy(
ascendd,
setup_fake_remote_control_service(Arc::new(expected_logs)),
&NodeId { id: 1234 },
);
Target::from_rcs_connection(conn).await.unwrap()
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_disabled() -> Result<()> {
let target = make_default_target(vec![]).await;
let t = target.downgrade();
let streamer = FakeDiagnosticsStreamer::new(1, Arc::new(Mutex::new(vec![])));
let logger = Logger::new_with_streamer_and_config(t, streamer, false);
logger.start().await.unwrap();
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_multiple_malformed_logs_in_series() -> Result<()> {
let target = make_default_target(vec![
FakeArchiveIteratorResponse {
values: vec!["log1".to_string(), "log2".to_string()],
iterator_error: None,
},
FakeArchiveIteratorResponse {
values: vec!["log3".to_string(), "log4".to_string()],
iterator_error: None,
},
])
.await;
let t = target.downgrade();
let log_buf = Arc::new(Mutex::new(vec![]));
let streamer = FakeDiagnosticsStreamer::new(0, log_buf.clone());
streamer.expect_setup(NODENAME, BOOT_TIME).await;
let logger = Logger::new_with_streamer_and_config(t, streamer, true);
logger.start().await.unwrap();
verify_logged(
log_buf.clone(),
vec![
logging_started_entry(),
malformed_log("log1"),
malformed_log("log2"),
malformed_log("log3"),
malformed_log("log4"),
],
)
.await;
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_multiple_valid_logs_in_series() -> Result<()> {
let log1 = target_log(1, "log1");
let log2 = target_log(2, "log2");
let log3 = target_log(3, "log3");
let log4 = target_log(4, "log4");
let target = make_default_target(vec![
FakeArchiveIteratorResponse {
values: vec![serde_json::to_string(&log1)?, serde_json::to_string(&log2)?],
iterator_error: None,
},
FakeArchiveIteratorResponse {
values: vec![serde_json::to_string(&log3)?, serde_json::to_string(&log4)?],
iterator_error: None,
},
])
.await;
let t = target.downgrade();
let log_buf = Arc::new(Mutex::new(vec![]));
let streamer = FakeDiagnosticsStreamer::new(0, log_buf.clone());
streamer.expect_setup(NODENAME, BOOT_TIME).await;
let logger = Logger::new_with_streamer_and_config(t, streamer, true);
logger.start().await.unwrap();
verify_logged(
log_buf.clone(),
vec![
logging_started_entry(),
valid_log(log1),
valid_log(log2),
valid_log(log3),
valid_log(log4),
],
)
.await;
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_skips_old_logs() -> Result<()> {
let log1 = target_log(1, "log1");
let log2 = target_log(2, "log2");
let target = make_default_target(vec![
FakeArchiveIteratorResponse {
values: vec![serde_json::to_string(&log1)?],
iterator_error: None,
},
FakeArchiveIteratorResponse {
values: vec![serde_json::to_string(&log2)?],
iterator_error: None,
},
])
.await;
let t = target.downgrade();
let log_buf = Arc::new(Mutex::new(vec![]));
let streamer = FakeDiagnosticsStreamer::new(1, log_buf.clone());
streamer.expect_setup(NODENAME, BOOT_TIME).await;
let logger = Logger::new_with_streamer_and_config(t, streamer, true);
logger.start().await.unwrap();
verify_logged(log_buf.clone(), vec![logging_started_entry(), valid_log(log2)]).await;
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_continues_after_error() -> Result<()> {
let log1 = target_log(1, "log1");
let log2 = target_log(2, "log2");
let target = make_default_target(vec![
FakeArchiveIteratorResponse {
values: vec![serde_json::to_string(&log1)?],
iterator_error: None,
},
FakeArchiveIteratorResponse {
values: vec![],
iterator_error: Some(ArchiveIteratorError::DataReadFailed),
},
FakeArchiveIteratorResponse {
values: vec![serde_json::to_string(&log2)?],
iterator_error: None,
},
])
.await;
let t = target.downgrade();
let log_buf = Arc::new(Mutex::new(vec![]));
let streamer = FakeDiagnosticsStreamer::new(0, log_buf.clone());
streamer.expect_setup(NODENAME, BOOT_TIME).await;
let logger = Logger::new_with_streamer_and_config(t, streamer, true);
logger.start().await.unwrap();
verify_logged(
log_buf.clone(),
vec![logging_started_entry(), valid_log(log1), valid_log(log2)],
)
.await;
Ok(())
}
}