Skip to content

Commit dd06f9c

Browse files
feat: add support for host logging, deprecating old topics_logger in favor of host-managed topics logger. updates examples. (#21)
1 parent 79bfe14 commit dd06f9c

23 files changed

+472
-326
lines changed

Cargo.lock

Lines changed: 53 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@ serde_json = { version = "1" }
3434
sha2 = { version = "0" }
3535
tiktoken-rs = { version = "0" }
3636
thiserror = { version = "2" }
37+
time = { version = "0.3" }
3738
wit-bindgen = { version = "0.41" }

momento-functions-host/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub mod aws;
1616
pub mod cache;
1717
pub mod encoding;
1818
pub mod http;
19+
pub mod logging;
1920
pub mod redis;
2021
mod spawn;
2122
pub mod topics;
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
//! Host interfaces for working with host logging, allowing you to send
2+
//! logs to different destinations
3+
use momento_functions_wit::host::momento::host::logging;
4+
use thiserror::Error;
5+
6+
/// Where do you want your logs to go?
7+
pub enum LogDestination {
8+
/// Momento topic within the same cache as your function
9+
Topic {
10+
/// Name of the topic
11+
topic: String,
12+
},
13+
/// AWS CloudWatch Log Group for your function's logs
14+
CloudWatch {
15+
/// ARN of the IAM role for Momento to assume
16+
iam_role_arn: String,
17+
/// ARN of the CloudWatch Log Group for Momento to publish your
18+
/// function logs to
19+
log_group_name: String,
20+
},
21+
}
22+
23+
impl LogDestination {
24+
/// Creates a Topic destination
25+
pub fn topic(name: impl Into<String>) -> Self {
26+
Self::Topic { topic: name.into() }
27+
}
28+
/// Creates a CloudWatch destination.
29+
/// Reach out to us at `[email protected]` for details on how to properly
30+
/// set up your log configuration.
31+
pub fn cloudwatch(iam_role_arn: impl Into<String>, log_group_name: impl Into<String>) -> Self {
32+
Self::CloudWatch {
33+
iam_role_arn: iam_role_arn.into(),
34+
log_group_name: log_group_name.into(),
35+
}
36+
}
37+
}
38+
39+
/// A single configuration for a destination
40+
pub struct LogConfiguration {
41+
/// At what level would you like your function's logs to be filtered into this destination?
42+
log_level: log::LevelFilter,
43+
/// At what level would you like Momento's system logs to be filtered into this destination?
44+
system_log_level: log::LevelFilter,
45+
/// The specific destination
46+
destination: LogDestination,
47+
}
48+
49+
impl LogConfiguration {
50+
/// Constructs a single logging input with a desired destination. System logs will be at default level (INFO).
51+
pub fn new(destination: LogDestination) -> Self {
52+
Self {
53+
log_level: log::LevelFilter::Info,
54+
system_log_level: log::LevelFilter::Info,
55+
destination,
56+
}
57+
}
58+
59+
/// Constructs a single logging input with a desired destination as well as a specified logs filter.
60+
pub fn with_log_level(mut self, log_level: log::LevelFilter) -> Self {
61+
self.log_level = log_level;
62+
self
63+
}
64+
65+
/// Constructs a single logging input with a desired destination as well as a specified system logs filter.
66+
pub fn with_system_log_level(mut self, system_log_level: log::LevelFilter) -> Self {
67+
self.system_log_level = system_log_level;
68+
self
69+
}
70+
}
71+
72+
impl From<LogDestination> for LogConfiguration {
73+
fn from(value: LogDestination) -> Self {
74+
match value {
75+
LogDestination::Topic { topic } => Self::new(LogDestination::topic(topic)),
76+
LogDestination::CloudWatch {
77+
iam_role_arn,
78+
log_group_name,
79+
} => Self::new(LogDestination::cloudwatch(iam_role_arn, log_group_name)),
80+
}
81+
}
82+
}
83+
84+
/// Create a single `LogConfiguration` given a `LogDestination`.
85+
pub fn log_configuration(destination: LogDestination) -> LogConfiguration {
86+
LogConfiguration::new(destination)
87+
}
88+
89+
impl From<LogDestination> for logging::Destination {
90+
fn from(value: LogDestination) -> Self {
91+
match value {
92+
LogDestination::Topic { topic } => {
93+
momento_functions_wit::host::momento::host::logging::Destination::Topic(
94+
logging::TopicDestination { topic_name: topic },
95+
)
96+
}
97+
LogDestination::CloudWatch {
98+
iam_role_arn,
99+
log_group_name,
100+
} => momento_functions_wit::host::momento::host::logging::Destination::Cloudwatch(
101+
logging::CloudwatchDestination {
102+
iam_role_arn,
103+
log_group_name,
104+
},
105+
),
106+
}
107+
}
108+
}
109+
110+
impl From<LogConfiguration> for logging::ConfigureLoggingInput {
111+
fn from(value: LogConfiguration) -> Self {
112+
Self {
113+
log_level: match value.log_level {
114+
log::LevelFilter::Off => logging::LogLevel::Off,
115+
log::LevelFilter::Error => logging::LogLevel::Error,
116+
log::LevelFilter::Warn => logging::LogLevel::Warn,
117+
log::LevelFilter::Info => logging::LogLevel::Info,
118+
log::LevelFilter::Debug => logging::LogLevel::Debug,
119+
// Momento does not publish Trace logs
120+
log::LevelFilter::Trace => logging::LogLevel::Debug,
121+
},
122+
system_logs_level: match value.system_log_level {
123+
log::LevelFilter::Off => logging::LogLevel::Off,
124+
log::LevelFilter::Error => logging::LogLevel::Error,
125+
log::LevelFilter::Warn => logging::LogLevel::Warn,
126+
log::LevelFilter::Info => logging::LogLevel::Info,
127+
log::LevelFilter::Debug => logging::LogLevel::Debug,
128+
// Momento does not publish Trace logs
129+
log::LevelFilter::Trace => logging::LogLevel::Debug,
130+
},
131+
destination: value.destination.into(),
132+
}
133+
}
134+
}
135+
136+
/// Captures any errors Momento has detected during log configuration
137+
#[derive(Debug, Error)]
138+
pub enum LogConfigurationError {
139+
#[error("Invalid auth provided for configuration! {message}")]
140+
/// Invalid auth was provided
141+
Auth {
142+
/// The error message bubbled back up
143+
message: String,
144+
},
145+
#[error("Something went wrong while trying to configure logs! {message}")]
146+
/// Something went wrong
147+
Unknown {
148+
/// The error message bubbled back up
149+
message: String,
150+
},
151+
}
152+
153+
impl From<logging::LogConfigurationError> for LogConfigurationError {
154+
fn from(value: logging::LogConfigurationError) -> Self {
155+
match value {
156+
logging::LogConfigurationError::Auth(e) => Self::Auth { message: e },
157+
}
158+
}
159+
}
160+
161+
/// Configures logging via Momento host functions
162+
pub fn configure_host_logging(
163+
configurations: impl IntoIterator<Item = LogConfiguration>,
164+
) -> Result<(), LogConfigurationError> {
165+
let configurations = configurations
166+
.into_iter()
167+
.map(|configuration| configuration.into())
168+
.collect::<Vec<logging::ConfigureLoggingInput>>();
169+
Ok(logging::configure_logging(&configurations)?)
170+
}
171+
172+
/// Logs a given string
173+
pub fn log(input: &str, level: log::Level) {
174+
logging::log(
175+
input,
176+
match level {
177+
log::Level::Error => logging::LogLevel::Error,
178+
log::Level::Warn => logging::LogLevel::Warn,
179+
log::Level::Info => logging::LogLevel::Info,
180+
log::Level::Debug => logging::LogLevel::Debug,
181+
// Momento does not publish Trace logs
182+
log::Level::Trace => logging::LogLevel::Debug,
183+
},
184+
)
185+
}

momento-functions-log/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ categories.workspace = true
1414
log = { workspace = true }
1515
momento-functions-host = { workspace = true }
1616
thiserror = { workspace = true }
17+
time = { workspace = true, features = ["formatting"] }
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use std::fmt::Write;
2+
3+
use log::{Log, set_logger_racy, set_max_level};
4+
use momento_functions_host::logging::{LogConfiguration, LogConfigurationError};
5+
use time::format_description::well_known::Rfc3339;
6+
7+
pub struct HostLog {}
8+
9+
impl HostLog {
10+
pub fn init(
11+
configurations: impl IntoIterator<Item = LogConfiguration>,
12+
) -> Result<(), LogConfigurationError> {
13+
static mut LOGGER: Option<HostLog> = None;
14+
// We're setting this to DEBUG so all logs are captured and sent to the host serving
15+
// the function. The host will determine whether to log the mesage.
16+
set_max_level(log::LevelFilter::Debug);
17+
momento_functions_host::logging::configure_host_logging(configurations)?;
18+
#[allow(static_mut_refs)]
19+
#[allow(clippy::expect_used)]
20+
// SAFETY: concurrency requirement is satisfied by the single threaded nature
21+
// of the Function environment.
22+
unsafe {
23+
LOGGER.replace(HostLog {});
24+
set_logger_racy(LOGGER.as_mut().expect("logger just set")).map_err(|e| {
25+
LogConfigurationError::Unknown {
26+
message: format!("Failed to configure logger! {e:?}"),
27+
}
28+
})
29+
}
30+
}
31+
}
32+
33+
impl Log for HostLog {
34+
fn enabled(&self, _metadata: &log::Metadata) -> bool {
35+
// Host logging will filter out logs based on level
36+
true
37+
}
38+
39+
fn log(&self, record: &log::Record) {
40+
let mut buffer = String::with_capacity(128);
41+
let utc_now = time::OffsetDateTime::now_utc();
42+
let timestamp = utc_now.format(&Rfc3339).unwrap_or("<unknown>".to_string());
43+
let record_level = record.level();
44+
let level = record_level.as_str();
45+
let module = record.module_path().unwrap_or("<unknown>");
46+
let file = record.file().unwrap_or("<unknown>");
47+
let line = record.line().unwrap_or(0);
48+
let log_message = record.args();
49+
50+
let _ = write!(
51+
&mut buffer,
52+
"{level} {timestamp} {module} {file}:{line} {log_message}"
53+
);
54+
55+
momento_functions_host::logging::log(buffer.as_str(), record_level);
56+
}
57+
58+
fn flush(&self) {}
59+
}

momento-functions-log/src/lib.rs

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,14 @@
1111
//! * [`momento-functions`](https://crates.io/crates/momento-functions): Code generators for Functions.
1212
//! * [`momento-functions-host`](https://crates.io/crates/momento-functions-host): Interfaces and tools for calling host interfaces.
1313
14-
use log::SetLoggerError;
15-
use thiserror::Error;
14+
use momento_functions_host::logging::{LogConfiguration, LogConfigurationError};
1615

17-
mod topic_logger;
16+
use crate::host_logging::HostLog;
17+
mod host_logging;
1818

19-
/// Which logging mode to use?
20-
pub enum LogMode {
21-
Topic {
22-
/// The topic to send logs to.
23-
///
24-
/// You can get the logs with the `momento` CLI, or on the Momento topics dashboard at gomomento.com.
25-
/// The CLI command would be `momento topic subscribe $topic`
26-
/// Log messages will stream to your terminal.
27-
topic: String,
28-
},
29-
}
30-
31-
#[derive(Debug, Error)]
32-
pub enum LogConfigError {
33-
#[error("Failed to initialize topics logger: {cause}")]
34-
TopicsInit { cause: SetLoggerError },
35-
}
36-
37-
/// Initializes the logging system with the specified log level and mode.
38-
///
39-
/// You **must** only call this function once.
40-
pub fn configure_logging(level: log::LevelFilter, mode: LogMode) -> Result<(), LogConfigError> {
41-
match mode {
42-
LogMode::Topic { topic } => topic_logger::TopicLog::init(level, topic)
43-
.map_err(|e| LogConfigError::TopicsInit { cause: e }),
44-
}
19+
/// Entrypoint for configuring logs to be delivered to a destination(s)
20+
pub fn configure_logs(
21+
configurations: impl IntoIterator<Item = LogConfiguration>,
22+
) -> Result<(), LogConfigurationError> {
23+
HostLog::init(configurations)
4524
}

0 commit comments

Comments
 (0)