Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
53 changes: 53 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 @@ -34,4 +34,5 @@ serde_json = { version = "1" }
sha2 = { version = "0" }
tiktoken-rs = { version = "0" }
thiserror = { version = "2" }
time = { version = "0.3" }
wit-bindgen = { version = "0.41" }
1 change: 1 addition & 0 deletions momento-functions-host/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub mod aws;
pub mod cache;
pub mod encoding;
pub mod http;
pub mod logging;
pub mod redis;
mod spawn;
pub mod topics;
Expand Down
102 changes: 102 additions & 0 deletions momento-functions-host/src/logging.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//! Host interfaces for working with host logging, allowing you to send
//! logs to different destinations
use momento_functions_wit::host::momento::host::logging;
use thiserror::Error;

/// Where do you want your logs to go?
pub enum LogDestination {
/// Momento topic within the same cache as your function
Topic {
/// Name of the topic
topic: String,
},
}

/// A single configuration for a destination
pub struct ConfigureLoggingInput {
/// At what level would you like Momento's system logs to be filtered into this destination?
pub system_log_level: log::LevelFilter,
/// The specific destination
pub destination: LogDestination,
}

impl ConfigureLoggingInput {
/// Constructs a single logging input with a desired destination. System logs will be at default level (INFO).
pub fn new(destination: LogDestination) -> Self {
Self {
system_log_level: log::LevelFilter::Info,
destination,
}
}

/// Constructs a single logging input with a desired destination as well as a specified system logs filter.
pub fn new_with_system_log_level(
system_log_level: log::LevelFilter,
destination: LogDestination,
) -> Self {
Self {
system_log_level,
destination,
}
}
}

impl From<LogDestination> for logging::Destination {
fn from(value: LogDestination) -> Self {
match value {
LogDestination::Topic { topic } => {
momento_functions_wit::host::momento::host::logging::Destination::Topic(
logging::TopicDestination { topic_name: topic },
)
}
}
}
}

impl From<ConfigureLoggingInput> for logging::ConfigureLoggingInput {
fn from(value: ConfigureLoggingInput) -> Self {
Self {
system_logs_level: match value.system_log_level {
log::LevelFilter::Off => logging::LogLevel::Off,
log::LevelFilter::Error => logging::LogLevel::Error,
log::LevelFilter::Warn => logging::LogLevel::Warn,
log::LevelFilter::Info => logging::LogLevel::Info,
log::LevelFilter::Debug => logging::LogLevel::Debug,
// Momento does not publish Trace logs
log::LevelFilter::Trace => logging::LogLevel::Debug,
},
destination: value.destination.into(),
}
}
}

/// Captures any errors Momento has detected during log configuration
#[derive(Debug, Error)]
pub enum LogConfigurationError {
#[error("Invalid auth provided for configuration! {message}")]
/// Invalid auth was provided
Auth {
/// The error message bubbled back up
message: String,
},
#[error("Something went wrong while trying to configure logs! {message}")]
/// Something went wrong
Unknown {
/// The error message bubbled back up
message: String,
},
}

/// Configures logging via Momento host functions
pub fn configure_logging(inputs: Vec<ConfigureLoggingInput>) -> Result<(), LogConfigurationError> {
let converted: Vec<logging::ConfigureLoggingInput> =
inputs.into_iter().map(Into::into).collect();
logging::configure_logging(&converted).map_err(|e| LogConfigurationError::Auth {
message: e.to_string(),
})
}

/// Logs a given string
pub fn log(input: &str) {
logging::log(input)
}
1 change: 1 addition & 0 deletions momento-functions-log/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ categories.workspace = true
log = { workspace = true }
momento-functions-host = { workspace = true }
thiserror = { workspace = true }
time = { workspace = true, features = ["formatting"] }
71 changes: 71 additions & 0 deletions momento-functions-log/src/host_logging.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use std::fmt::Write;
use std::sync::atomic::AtomicU32;

use log::{LevelFilter, Log, set_logger_racy, set_max_level};
use momento_functions_host::logging::{ConfigureLoggingInput, LogConfigurationError};
use time::format_description::well_known::Rfc3339;

pub struct HostLog {
level: LevelFilter,
dropped: AtomicU32,
}

impl HostLog {
pub fn init(
log_level: LevelFilter,
destinations: Vec<ConfigureLoggingInput>,
) -> Result<(), LogConfigurationError> {
set_max_level(log_level);

static mut LOGGER: Option<HostLog> = None;
momento_functions_host::logging::configure_logging(destinations)?;
#[allow(static_mut_refs)]
#[allow(clippy::expect_used)]
unsafe {
LOGGER.replace(HostLog {
level: log_level,
dropped: AtomicU32::new(0),
});
set_logger_racy(LOGGER.as_mut().expect("logger just set")).map_err(|e| {
LogConfigurationError::Unknown {
message: format!("Failed to configure logger! {e:?}"),
}
})
}
}
}

impl Log for HostLog {
fn enabled(&self, metadata: &log::Metadata) -> bool {
metadata.level() <= self.level
}

fn log(&self, record: &log::Record) {
if self.enabled(record.metadata()) {
let mut buffer = String::with_capacity(128);
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder about this length restriction for the host-logging approach. Is it still appropriate?

Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't a restriction, it's an initial capacity to help avoid excessive reallocation. That said, we could reach for unsafe here too and reuse the buffer given the single-thread nature of Functions... Let's leave that for another time though :-)

let utc_now = time::OffsetDateTime::now_utc();
let timestamp = utc_now.format(&Rfc3339).unwrap_or("<unknown>".to_string());
let level = record.level().as_str();
let module = record.module_path().unwrap_or("<unknown>");
let file = record.file().unwrap_or("<unknown>");
let line = record.line().unwrap_or(0);
let log_message = record.args();

let dropped = self.dropped.swap(0, std::sync::atomic::Ordering::Relaxed);
let dropped_clause = if 0 < dropped {
format!(" ({dropped} messages dropped)")
} else {
String::new()
};

let _ = write!(
&mut buffer,
"{level} {timestamp} {module} {file}:{line}{dropped_clause} {log_message}"
);

momento_functions_host::logging::log(buffer.as_str());
}
}

fn flush(&self) {}
}
32 changes: 10 additions & 22 deletions momento-functions-log/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,23 @@
//! * [`momento-functions`](https://crates.io/crates/momento-functions): Code generators for Functions.
//! * [`momento-functions-host`](https://crates.io/crates/momento-functions-host): Interfaces and tools for calling host interfaces.
use log::SetLoggerError;
use momento_functions_host::logging::{ConfigureLoggingInput, LogConfigurationError};
use thiserror::Error;

mod topic_logger;

/// Which logging mode to use?
pub enum LogMode {
Topic {
/// The topic to send logs to.
///
/// You can get the logs with the `momento` CLI, or on the Momento topics dashboard at gomomento.com.
/// The CLI command would be `momento topic subscribe $topic`
/// Log messages will stream to your terminal.
topic: String,
},
}
mod host_logging;

#[derive(Debug, Error)]
pub enum LogConfigError {
#[error("Failed to initialize topics logger: {cause}")]
TopicsInit { cause: SetLoggerError },
#[error("Failed to initialize logger: {cause}")]
Init { cause: LogConfigurationError },
}

/// Initializes the logging system with the specified log level and mode.
/// Initializes the logging system with the specified log level and destinations.
///
/// You **must** only call this function once.
pub fn configure_logging(level: log::LevelFilter, mode: LogMode) -> Result<(), LogConfigError> {
match mode {
LogMode::Topic { topic } => topic_logger::TopicLog::init(level, topic)
.map_err(|e| LogConfigError::TopicsInit { cause: e }),
}
pub fn configure_logging(
level: log::LevelFilter,
destinations: Vec<ConfigureLoggingInput>,
) -> Result<(), LogConfigError> {
host_logging::HostLog::init(level, destinations).map_err(|e| LogConfigError::Init { cause: e })
}
68 changes: 0 additions & 68 deletions momento-functions-log/src/topic_logger.rs

This file was deleted.

Loading