Skip to content
Open
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
1 change: 1 addition & 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 src/hyperlight_common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ tracing = { version = "0.1.44", optional = true }
arbitrary = {version = "1.4.2", optional = true, features = ["derive"]}
spin = "0.10.0"
thiserror = { version = "2.0.18", default-features = false }
tracing-core = { version = "0.1.36", default-features = false }

[features]
default = ["tracing"]
Expand Down
3 changes: 3 additions & 0 deletions src/hyperlight_common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ mod flatbuffers;
// cbindgen:ignore
pub mod layout;

// cbindgen:ignore
pub mod log_level;

/// cbindgen:ignore
pub mod mem;

Expand Down
109 changes: 109 additions & 0 deletions src/hyperlight_common/src/log_level.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
Copyright 2025 The Hyperlight Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/// This type is a unified definition of log level filters between the guest and host.
///
/// This is needed because currently the guest uses both the `log` and `tracing` crates,
/// and needs each type of `LevelFilter` from both crates.
///
/// To avoid as much as possible the amount of conversions between the two types, we define a
/// single type that can be converted to both `log::LevelFilter` and `tracing::LevelFilter`.
/// NOTE: This also takes care of the fact that the `tracing` and `log` enum types for the log
/// levels are not guaranteed to have the same discriminants, so we can't just cast between them.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GuestLogFilter {
Off,
Error,
Warn,
Info,
Debug,
Trace,
}

impl From<GuestLogFilter> for tracing_core::LevelFilter {
fn from(filter: GuestLogFilter) -> Self {
match filter {
GuestLogFilter::Off => tracing_core::LevelFilter::OFF,
GuestLogFilter::Error => tracing_core::LevelFilter::ERROR,
GuestLogFilter::Warn => tracing_core::LevelFilter::WARN,
GuestLogFilter::Info => tracing_core::LevelFilter::INFO,
GuestLogFilter::Debug => tracing_core::LevelFilter::DEBUG,
GuestLogFilter::Trace => tracing_core::LevelFilter::TRACE,
}
}
}

impl From<GuestLogFilter> for log::LevelFilter {
fn from(filter: GuestLogFilter) -> Self {
match filter {
GuestLogFilter::Off => log::LevelFilter::Off,
GuestLogFilter::Error => log::LevelFilter::Error,
GuestLogFilter::Warn => log::LevelFilter::Warn,
GuestLogFilter::Info => log::LevelFilter::Info,
GuestLogFilter::Debug => log::LevelFilter::Debug,
GuestLogFilter::Trace => log::LevelFilter::Trace,
}
}
}

/// Used by the host to convert a [`tracing_core::LevelFilter`] to the intermediary [`GuestLogFilter`]
/// filter that is later converted to `u64` and passed to the guest via the C API.
impl From<tracing_core::LevelFilter> for GuestLogFilter {
fn from(value: tracing_core::LevelFilter) -> Self {
match value {
tracing_core::LevelFilter::OFF => Self::Off,
tracing_core::LevelFilter::ERROR => Self::Error,
tracing_core::LevelFilter::WARN => Self::Warn,
tracing_core::LevelFilter::INFO => Self::Info,
tracing_core::LevelFilter::DEBUG => Self::Debug,
tracing_core::LevelFilter::TRACE => Self::Trace,
}
}
}

/// Used by the guest to convert a `u64` value passed from the host via the C API to the
/// intermediary [`GuestLogFilter`] filter that is later converted to both
/// `tracing_core::LevelFilter` and `log::LevelFilter`.
impl TryFrom<u64> for GuestLogFilter {
type Error = ();

fn try_from(value: u64) -> Result<Self, <GuestLogFilter as TryFrom<u64>>::Error> {
match value {
0 => Ok(Self::Off),
1 => Ok(Self::Error),
2 => Ok(Self::Warn),
3 => Ok(Self::Info),
4 => Ok(Self::Debug),
5 => Ok(Self::Trace),
_ => Err(()),
}
}
}

/// Used by the host to convert the [`GuestLogFilter`] to a `u64` that is passed to the guest via
/// the C API.
impl From<GuestLogFilter> for u64 {
fn from(value: GuestLogFilter) -> Self {
match value {
GuestLogFilter::Off => 0,
GuestLogFilter::Error => 1,
GuestLogFilter::Warn => 2,
GuestLogFilter::Info => 3,
GuestLogFilter::Debug => 4,
GuestLogFilter::Trace => 5,
}
}
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding unit tests for the GuestLogFilter conversions to ensure bidirectional conversion correctness between u64, GuestLogFilter, log::LevelFilter, and tracing_core::LevelFilter. This would help prevent regressions if the underlying crate enums change their discriminant values.

Suggested change
}
}
#[cfg(test)]
mod tests {
use super::GuestLogFilter;
#[test]
fn guest_log_filter_u64_roundtrip() {
let variants = [
GuestLogFilter::Off,
GuestLogFilter::Error,
GuestLogFilter::Warn,
GuestLogFilter::Info,
GuestLogFilter::Debug,
GuestLogFilter::Trace,
];
for variant in variants {
let as_u64: u64 = variant.into();
let back = GuestLogFilter::try_from(as_u64).expect("conversion from u64 should succeed");
assert_eq!(variant, back);
}
}
#[test]
fn guest_log_filter_tracing_roundtrip() {
let variants = [
GuestLogFilter::Off,
GuestLogFilter::Error,
GuestLogFilter::Warn,
GuestLogFilter::Info,
GuestLogFilter::Debug,
GuestLogFilter::Trace,
];
for variant in variants {
let tracing_filter: tracing_core::LevelFilter = variant.into();
let back: GuestLogFilter = tracing_filter.into();
assert_eq!(variant, back);
}
}
#[test]
fn guest_log_filter_try_from_u64_rejects_invalid() {
// Any value outside the defined range [0, 5] should be rejected.
assert!(GuestLogFilter::try_from(u64::MAX).is_err());
assert!(GuestLogFilter::try_from(6).is_err());
}
}

Copilot uses AI. Check for mistakes.
2 changes: 1 addition & 1 deletion src/hyperlight_guest/src/exit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use tracing::instrument;
/// this means we can instrument it as a trace point because the trace state
/// shall not be locked at this point (we are not in an exception context).
#[inline(never)]
#[instrument(skip_all, level = "Trace")]
#[instrument(skip_all, level = "Info")]
pub fn halt() {
#[cfg(all(feature = "trace_guest", target_arch = "x86_64"))]
{
Expand Down
4 changes: 3 additions & 1 deletion src/hyperlight_guest/src/guest_handle/host_comm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ impl GuestHandle {
///
/// When calling `call_host_function<T>`, this function is called
/// internally to get the return value.
#[instrument(skip_all, level = "Trace")]
pub fn get_host_return_value<T: TryFrom<ReturnValue>>(&self) -> Result<T> {
let inner = self
.try_pop_shared_input_data_into::<FunctionCallResult>()
Expand Down Expand Up @@ -106,6 +107,7 @@ impl GuestHandle {
///
/// Note: The function return value must be obtained by calling
/// `get_host_return_value`.
#[instrument(skip_all, level = "Trace")]
pub fn call_host_function_without_returning_result(
&self,
function_name: &str,
Expand Down Expand Up @@ -139,7 +141,7 @@ impl GuestHandle {
/// sends it to the host, and then retrieves the return value.
///
/// The return value is deserialized into the specified type `T`.
#[instrument(skip_all, level = "Trace")]
#[instrument(skip_all, level = "Info")]
pub fn call_host_function<T: TryFrom<ReturnValue>>(
&self,
function_name: &str,
Expand Down
4 changes: 2 additions & 2 deletions src/hyperlight_guest/src/guest_handle/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ use core::any::type_name;
use core::slice::from_raw_parts_mut;

use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
use tracing::{Span, instrument};
use tracing::instrument;

use super::handle::GuestHandle;
use crate::error::{HyperlightGuestError, Result};

impl GuestHandle {
/// Pops the top element from the shared input data buffer and returns it as a T
#[instrument(skip_all, parent = Span::current(), level= "Trace")]
#[instrument(skip_all, level = "Trace")]
pub fn try_pop_shared_input_data_into<T>(&self) -> Result<T>
where
T: for<'a> TryFrom<&'a [u8]>,
Expand Down
8 changes: 4 additions & 4 deletions src/hyperlight_guest_bin/src/guest_function/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ use hyperlight_common::flatbuffer_wrappers::function_types::{FunctionCallResult,
use hyperlight_common::flatbuffer_wrappers::guest_error::{ErrorCode, GuestError};
use hyperlight_guest::error::{HyperlightGuestError, Result};
use hyperlight_guest::exit::halt;
use tracing::{Span, instrument};
use tracing::instrument;

use crate::{GUEST_HANDLE, REGISTERED_GUEST_FUNCTIONS};

type GuestFunc = fn(&FunctionCall) -> Result<Vec<u8>>;

#[instrument(skip_all, parent = Span::current(), level= "Trace")]
#[instrument(skip_all, level = "Info")]
pub(crate) fn call_guest_function(function_call: FunctionCall) -> Result<Vec<u8>> {
// Validate this is a Guest Function Call
if function_call.function_call_type() != FunctionCallType::Guest {
Expand Down Expand Up @@ -85,7 +85,7 @@ pub(crate) fn call_guest_function(function_call: FunctionCall) -> Result<Vec<u8>
// This function may panic, as we have no other ways of dealing with errors at this level
#[unsafe(no_mangle)]
#[inline(never)]
#[instrument(skip_all, parent = Span::current(), level= "Trace")]
#[instrument(skip_all, level = "Trace")]
fn internal_dispatch_function() {
let handle = unsafe { GUEST_HANDLE };

Expand Down Expand Up @@ -119,7 +119,7 @@ fn internal_dispatch_function() {
// This is implemented as a separate function to make sure that epilogue in the internal_dispatch_function is called before the halt()
// which if it were included in the internal_dispatch_function cause the epilogue to not be called because the halt() would not return
// when running in the hypervisor.
#[instrument(skip_all, parent = Span::current(), level= "Trace")]
#[instrument(skip_all, level = "Info")]
pub(crate) extern "C" fn dispatch_function() {
// The hyperlight host likes to use one partition and reset it in
// various ways; if that has happened, there might stale TLB
Expand Down
4 changes: 2 additions & 2 deletions src/hyperlight_guest_bin/src/guest_logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ use crate::GUEST_HANDLE;
// this is private on purpose so that `log` can only be called though the `log!` macros.
struct GuestLogger {}

pub(crate) fn init_logger(level: LevelFilter) {
pub(crate) fn init_logger(filter: LevelFilter) {
// if this `expect` fails we have no way to recover anyway, so we actually prefer a panic here
// below temporary guest logger is promoted to static by the compiler.
log::set_logger(&GuestLogger {}).expect("unable to setup guest logger");
log::set_max_level(level);
log::set_max_level(filter);
}

impl log::Log for GuestLogger {
Expand Down
16 changes: 9 additions & 7 deletions src/hyperlight_guest_bin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ use guest_function::call::dispatch_function;
use guest_function::register::GuestFunctionRegister;
use guest_logger::init_logger;
use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
use hyperlight_common::log_level::GuestLogFilter;
use hyperlight_common::mem::HyperlightPEB;
#[cfg(feature = "mem_profile")]
use hyperlight_common::outb::OutBAction;
use hyperlight_guest::exit::write_abort;
use hyperlight_guest::guest_handle::handle::GuestHandle;
use log::LevelFilter;

// === Modules ===
#[cfg_attr(target_arch = "x86_64", path = "arch/amd64/mod.rs")]
Expand Down Expand Up @@ -212,15 +212,17 @@ pub(crate) extern "C" fn generic_init(peb_address: u64, seed: u64, ops: u64, max
}

// set up the logger
let max_log_level = LevelFilter::iter()
.nth(max_log_level as usize)
.expect("Invalid log level");
init_logger(max_log_level);
let guest_log_level_filter =
GuestLogFilter::try_from(max_log_level).expect("Invalid log level");
init_logger(guest_log_level_filter.into());

// It is important that all the tracing events are produced after the tracing is initialized.
#[cfg(feature = "trace_guest")]
if max_log_level != LevelFilter::Off {
hyperlight_guest_tracing::init_guest_tracing(guest_start_tsc);
if guest_log_level_filter != GuestLogFilter::Off {
hyperlight_guest_tracing::init_guest_tracing(
guest_start_tsc,
guest_log_level_filter.into(),
);
}

#[cfg(feature = "macros")]
Expand Down
2 changes: 1 addition & 1 deletion src/hyperlight_guest_tracing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ description = """Provides the tracing functionality for the hyperlight guest."""
[dependencies]
hyperlight-common = { workspace = true, default-features = false }
spin = "0.10.0"
tracing = { version = "0.1.44", default-features = false, features = ["attributes"] }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you mean to include this added feature log? https://docs.rs/tracing/latest/tracing/#emitting-log-records. I thought we wanted to switch to tracing

tracing = { version = "0.1.44", default-features = false, features = ["attributes", "log"] }
tracing-core = { version = "0.1.36", default-features = false }

[lints]
Expand Down
5 changes: 3 additions & 2 deletions src/hyperlight_guest_tracing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ mod trace {
use alloc::sync::{Arc, Weak};

use spin::Mutex;
use tracing_core::LevelFilter;

use crate::state::GuestState;
use crate::subscriber::GuestSubscriber;
Expand All @@ -61,12 +62,12 @@ mod trace {
static GUEST_STATE: spin::Once<Weak<Mutex<GuestState>>> = spin::Once::new();

/// Initialize the guest tracing subscriber as global default.
pub fn init_guest_tracing(guest_start_tsc: u64) {
pub fn init_guest_tracing(guest_start_tsc: u64, max_log_level: LevelFilter) {
// Set as global default if not already set.
if tracing_core::dispatcher::has_been_set() {
return;
}
let sub = GuestSubscriber::new(guest_start_tsc);
let sub = GuestSubscriber::new(guest_start_tsc, max_log_level);
let state = sub.state();
// Store state Weak<GuestState> to use later at runtime
GUEST_STATE.call_once(|| Arc::downgrade(state));
Expand Down
15 changes: 11 additions & 4 deletions src/hyperlight_guest_tracing/src/subscriber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use alloc::sync::Arc;
use spin::Mutex;
use tracing_core::span::{Attributes, Id, Record};
use tracing_core::subscriber::Subscriber;
use tracing_core::{Event, Metadata};
use tracing_core::{Event, LevelFilter, Metadata};

use crate::state::GuestState;

Expand All @@ -31,22 +31,29 @@ pub(crate) struct GuestSubscriber {
/// A reference to this state is stored in a static variable
/// so it can be accessed from the guest tracing API
state: Arc<Mutex<GuestState>>,
/// Maximum log level to record
max_log_level: LevelFilter,
}

impl GuestSubscriber {
pub(crate) fn new(guest_start_tsc: u64) -> Self {
/// Creates a new `GuestSubscriber` with the given guest start TSC and maximum log level
pub(crate) fn new(guest_start_tsc: u64, filter: LevelFilter) -> Self {
Self {
state: Arc::new(Mutex::new(GuestState::new(guest_start_tsc))),
max_log_level: filter,
}
}
/// Returns a reference to the internal state of the subscriber
/// This is used to access the spans and events collected by the subscriber
pub(crate) fn state(&self) -> &Arc<Mutex<GuestState>> {
&self.state
}
}

impl Subscriber for GuestSubscriber {
fn enabled(&self, _md: &Metadata<'_>) -> bool {
true
fn enabled(&self, md: &Metadata<'_>) -> bool {
// Check if the metadata level is less than or equal to the maximum log level filter
self.max_log_level >= *md.level()
}

fn new_span(&self, attrs: &Attributes<'_>) -> Id {
Expand Down
Loading