Skip to content

Commit 2b46a02

Browse files
authored
Merge pull request #112 from CodeGov-org/fix/logs-pagination
fix: logs timestamp indexing in memory
2 parents 91a9c29 + f5acad8 commit 2b46a02

File tree

9 files changed

+183
-94
lines changed

9 files changed

+183
-94
lines changed

src/backend/impl/src/controllers/log_controller.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,7 @@ impl<A: AccessControlService, L: LogService> LogController<A, L> {
5050
self.access_control_service
5151
.assert_principal_is_admin(&calling_principal)?;
5252

53-
let logs = self.log_service.list_logs(request);
54-
55-
Ok(logs)
53+
self.log_service.list_logs(request)
5654
}
5755
}
5856

@@ -128,7 +126,7 @@ mod tests {
128126
.expect_list_logs()
129127
.once()
130128
.with(eq(request.clone()))
131-
.return_const(logs.clone());
129+
.return_const(Ok(logs.clone()));
132130

133131
let controller = LogController::new(access_control_service_mock, service_mock);
134132

src/backend/impl/src/fixtures/log.rs

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,30 +34,6 @@ pub fn log_entry_error() -> LogEntry {
3434
}
3535
}
3636

37-
#[fixture]
38-
pub fn log_entries() -> Vec<LogEntry> {
39-
vec![
40-
LogEntry {
41-
date_time: date_time_a(),
42-
level: LogLevel::Info,
43-
context: Some("function_a".to_string()),
44-
message: "foo".to_string(),
45-
},
46-
LogEntry {
47-
date_time: date_time_b(),
48-
level: LogLevel::Warn,
49-
context: Some("function_b".to_string()),
50-
message: "bar".to_string(),
51-
},
52-
LogEntry {
53-
date_time: date_time_b(),
54-
level: LogLevel::Error,
55-
context: Some("function_c".to_string()),
56-
message: "baz".to_string(),
57-
},
58-
]
59-
}
60-
6137
pub mod filters {
6238
use crate::repositories::LogsFilter;
6339

src/backend/impl/src/repositories/log_repository.rs

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
use super::{init_logs, LogEntry, LogId, LogMemory};
1+
use super::{
2+
init_log_timestamp_index, init_logs, DateTime, LogEntry, LogId, LogMemory,
3+
LogTimestampIndexMemory, LogTimestampKey, LogTimestampRange,
4+
};
25
use backend_api::ApiError;
36
use std::cell::RefCell;
47

58
#[cfg_attr(test, mockall::automock)]
69
pub trait LogRepository {
7-
fn get_logs(&self) -> Vec<LogEntry>;
10+
fn get_logs(
11+
&self,
12+
after: Option<DateTime>,
13+
before: Option<DateTime>,
14+
) -> Result<Vec<LogEntry>, ApiError>;
815

916
fn append_log(&self, log_entry: LogEntry) -> Result<LogId, ApiError>;
1017
}
@@ -18,14 +25,34 @@ impl Default for LogRepositoryImpl {
1825
}
1926

2027
impl LogRepository for LogRepositoryImpl {
21-
fn get_logs(&self) -> Vec<LogEntry> {
22-
STATE.with_borrow(|s| s.logs.iter().collect::<Vec<_>>())
28+
fn get_logs(
29+
&self,
30+
after: Option<DateTime>,
31+
before: Option<DateTime>,
32+
) -> Result<Vec<LogEntry>, ApiError> {
33+
let range = LogTimestampRange::new(after, before)?;
34+
let logs = STATE.with_borrow(|s| {
35+
s.logs_timestamp_index
36+
.range(range)
37+
.filter_map(|(_, log_id)| {
38+
// the None case should never happen
39+
s.logs.get(log_id)
40+
})
41+
.collect()
42+
});
43+
Ok(logs)
2344
}
2445

2546
fn append_log(&self, log_entry: LogEntry) -> Result<LogId, ApiError> {
26-
STATE
27-
.with_borrow_mut(|s| s.logs.append(&log_entry))
28-
.map_err(|e| ApiError::internal(&format!("Cannot write log: {:?}", e)))
47+
STATE.with_borrow_mut(|s| {
48+
let log_id = s
49+
.logs
50+
.append(&log_entry)
51+
.map_err(|e| ApiError::internal(&format!("Cannot write log: {:?}", e)))?;
52+
let log_key = LogTimestampKey::new(log_entry.date_time, log_id)?;
53+
s.logs_timestamp_index.insert(log_key, log_id);
54+
Ok(log_id)
55+
})
2956
}
3057
}
3158

@@ -37,11 +64,15 @@ impl LogRepositoryImpl {
3764

3865
struct LogState {
3966
logs: LogMemory,
67+
logs_timestamp_index: LogTimestampIndexMemory,
4068
}
4169

4270
impl Default for LogState {
4371
fn default() -> Self {
44-
Self { logs: init_logs() }
72+
Self {
73+
logs: init_logs(),
74+
logs_timestamp_index: init_log_timestamp_index(),
75+
}
4576
}
4677
}
4778

@@ -52,23 +83,28 @@ thread_local! {
5283
#[cfg(test)]
5384
mod tests {
5485
use super::*;
55-
use crate::fixtures;
86+
use crate::{fixtures, repositories::LogsFilter};
5687
use rstest::*;
5788

5889
#[rstest]
59-
async fn get_logs() {
90+
#[case::before_filter_matching(fixtures::filters::before_filter_matching())]
91+
#[case::before_filter_not_matching(fixtures::filters::before_filter_not_matching())]
92+
#[case::after_filter_matching(fixtures::filters::after_filter_matching())]
93+
#[case::after_filter_not_matching(fixtures::filters::after_filter_not_matching())]
94+
#[case::time_range_filter_matching(fixtures::filters::time_range_filter_matching())]
95+
#[case::time_range_filter_not_matching(fixtures::filters::time_range_filter_not_matching())]
96+
async fn get_logs(#[case] fixture: (LogEntry, LogsFilter, bool)) {
97+
let (log_entry, filter, expected) = fixture;
98+
6099
STATE.set(LogState::default());
61100

62-
let log_entries = fixtures::log_entries();
63101
let repository = LogRepositoryImpl::default();
102+
repository.append_log(log_entry.clone()).unwrap();
64103

65-
for log_entry in log_entries.iter() {
66-
repository.append_log(log_entry.clone()).unwrap();
67-
}
68-
69-
let result = repository.get_logs();
104+
// ranges are tested in the service and controller above
105+
let result = repository.get_logs(filter.after, filter.before).unwrap();
70106

71-
assert_eq!(result, log_entries);
107+
assert_eq!(result, if expected { vec![log_entry] } else { vec![] });
72108
}
73109

74110
#[rstest]
Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
1-
use super::{Memory, LOGS_INDEX_MEMORY_ID, LOGS_MEMORY_ID, MEMORY_MANAGER};
2-
use crate::repositories::LogEntry;
3-
use ic_stable_structures::Log;
1+
use super::{
2+
Memory, LOGS_INDEX_MEMORY_ID, LOGS_MEMORY_ID, LOGS_TIMESTAMP_LEVEL_INDEX_MEMORY_ID,
3+
MEMORY_MANAGER,
4+
};
5+
use crate::repositories::{LogEntry, LogId, LogTimestampKey};
6+
use ic_stable_structures::{BTreeMap, Log};
47

58
pub type LogMemory = Log<LogEntry, Memory, Memory>;
9+
pub type LogTimestampIndexMemory = BTreeMap<LogTimestampKey, LogId, Memory>;
610

711
pub fn init_logs() -> LogMemory {
812
// TODO: handle the error
913
LogMemory::init(get_logs_index_memory(), get_logs_memory()).unwrap()
1014
}
1115

16+
pub fn init_log_timestamp_index() -> LogTimestampIndexMemory {
17+
LogTimestampIndexMemory::init(get_logs_timestamp_level_index_memory())
18+
}
19+
1220
fn get_logs_index_memory() -> Memory {
1321
MEMORY_MANAGER.with(|m| m.borrow().get(LOGS_INDEX_MEMORY_ID))
1422
}
1523

1624
fn get_logs_memory() -> Memory {
1725
MEMORY_MANAGER.with(|m| m.borrow().get(LOGS_MEMORY_ID))
1826
}
27+
28+
fn get_logs_timestamp_level_index_memory() -> Memory {
29+
MEMORY_MANAGER.with(|m| m.borrow().get(LOGS_TIMESTAMP_LEVEL_INDEX_MEMORY_ID))
30+
}

src/backend/impl/src/repositories/memories/memory_manager.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ pub(super) const PROPOSAL_REVIEW_COMMIT_PROPOSAL_REVIEW_ID_USER_ID_MEMORY_ID: Me
2828
pub(super) const IMAGES_MEMORY_ID: MemoryId = MemoryId::new(13);
2929
pub(super) const PROPOSAL_NERVOUS_SYSTEM_ID_INDEX_MEMORY_ID: MemoryId = MemoryId::new(14);
3030
pub(super) const PROPOSAL_TIMESTAMP_INDEX_MEMORY_ID: MemoryId = MemoryId::new(15);
31+
pub(super) const LOGS_TIMESTAMP_LEVEL_INDEX_MEMORY_ID: MemoryId = MemoryId::new(16);

src/backend/impl/src/repositories/types/log.rs

Lines changed: 87 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
use super::DateTime;
2+
use backend_api::ApiError;
23
use candid::{CandidType, Decode, Deserialize, Encode};
3-
use ic_stable_structures::{storable::Bound, Storable};
4-
use std::borrow::Cow;
4+
use ic_stable_structures::{
5+
storable::{Blob, Bound},
6+
Storable,
7+
};
8+
use std::{borrow::Cow, ops::RangeBounds};
59

610
pub type LogId = u64;
711

@@ -16,18 +20,6 @@ pub struct LogsFilter {
1620

1721
impl LogsFilter {
1822
pub fn matches(&self, log_entry: &LogEntry) -> bool {
19-
if let Some(before) = &self.before {
20-
if log_entry.date_time > *before {
21-
return false;
22-
}
23-
}
24-
25-
if let Some(after) = &self.after {
26-
if log_entry.date_time < *after {
27-
return false;
28-
}
29-
}
30-
3123
if let Some(level) = &self.level {
3224
if log_entry.level != *level {
3325
return false;
@@ -82,10 +74,77 @@ impl Storable for LogEntry {
8274
const BOUND: Bound = Bound::Unbounded;
8375
}
8476

77+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
78+
pub struct LogTimestampKey(Blob<{ Self::MAX_SIZE as usize }>);
79+
80+
impl LogTimestampKey {
81+
const MAX_SIZE: u32 = <(DateTime, LogId)>::BOUND.max_size();
82+
83+
pub fn new(date_time: DateTime, log_id: LogId) -> Result<Self, ApiError> {
84+
Ok(Self(
85+
Blob::try_from((date_time, log_id).to_bytes().as_ref()).map_err(|_| {
86+
ApiError::internal(&format!(
87+
"Failed to convert date time {:?} and log id {} to bytes.",
88+
date_time, log_id
89+
))
90+
})?,
91+
))
92+
}
93+
}
94+
95+
impl Storable for LogTimestampKey {
96+
fn to_bytes(&self) -> Cow<[u8]> {
97+
self.0.to_bytes()
98+
}
99+
100+
fn from_bytes(bytes: Cow<[u8]>) -> Self {
101+
Self(Blob::from_bytes(bytes))
102+
}
103+
104+
const BOUND: Bound = Bound::Bounded {
105+
max_size: Self::MAX_SIZE,
106+
is_fixed_size: true,
107+
};
108+
}
109+
110+
pub struct LogTimestampRange {
111+
start_bound: LogTimestampKey,
112+
end_bound: LogTimestampKey,
113+
}
114+
115+
impl LogTimestampRange {
116+
pub fn new(
117+
min_date_time: Option<DateTime>,
118+
max_date_time: Option<DateTime>,
119+
) -> Result<Self, ApiError> {
120+
let max_date_time = match max_date_time {
121+
Some(max_date_time) => max_date_time,
122+
None => DateTime::max()?,
123+
};
124+
Ok(Self {
125+
start_bound: LogTimestampKey::new(
126+
min_date_time.unwrap_or_else(DateTime::min),
127+
LogId::MIN,
128+
)?,
129+
end_bound: LogTimestampKey::new(max_date_time, LogId::MAX)?,
130+
})
131+
}
132+
}
133+
134+
impl RangeBounds<LogTimestampKey> for LogTimestampRange {
135+
fn start_bound(&self) -> std::ops::Bound<&LogTimestampKey> {
136+
std::ops::Bound::Included(&self.start_bound)
137+
}
138+
139+
fn end_bound(&self) -> std::ops::Bound<&LogTimestampKey> {
140+
std::ops::Bound::Included(&self.end_bound)
141+
}
142+
}
143+
85144
#[cfg(test)]
86145
mod tests {
87146
use super::*;
88-
use crate::fixtures;
147+
use crate::{fixtures, system_api::get_date_time};
89148
use rstest::*;
90149

91150
#[rstest]
@@ -99,23 +158,27 @@ mod tests {
99158

100159
#[rstest]
101160
#[case::empty_filter(fixtures::filters::empty_filter())]
102-
#[case::before_filter_matching(fixtures::filters::before_filter_matching())]
103-
#[case::before_filter_not_matching(fixtures::filters::before_filter_not_matching())]
104-
#[case::after_filter_matching(fixtures::filters::after_filter_matching())]
105-
#[case::after_filter_not_matching(fixtures::filters::after_filter_not_matching())]
106-
#[case::time_range_filter_matching(fixtures::filters::time_range_filter_matching())]
107-
#[case::time_range_filter_not_matching(fixtures::filters::time_range_filter_not_matching())]
108161
#[case::level_filter_matching(fixtures::filters::level_filter_matching())]
109162
#[case::level_filter_not_matching(fixtures::filters::level_filter_not_matching())]
110163
#[case::context_filter_matching(fixtures::filters::context_filter_matching())]
111164
#[case::context_filter_not_matching(fixtures::filters::context_filter_not_matching())]
112165
#[case::message_filter_matching(fixtures::filters::message_filter_matching())]
113-
#[case::message_filter_not_matchingd(fixtures::filters::message_filter_not_matching())]
114-
#[case::all_matching(fixtures::filters::all_matching())]
115-
#[case::all_not_matching(fixtures::filters::all_not_matching())]
166+
#[case::message_filter_not_matching(fixtures::filters::message_filter_not_matching())]
116167
fn filter_matches(#[case] fixture: (LogEntry, LogsFilter, bool)) {
117168
let (log_entry, filter, expected) = fixture;
118169

119170
assert_eq!(filter.matches(&log_entry), expected);
120171
}
172+
173+
#[rstest]
174+
fn log_timestamp_key_storable_impl() {
175+
let date_time = get_date_time().unwrap();
176+
let log_id: LogId = 1234;
177+
178+
let key = LogTimestampKey::new(DateTime::new(date_time).unwrap(), log_id).unwrap();
179+
let serialized_key = key.to_bytes();
180+
let deserialized_key = LogTimestampKey::from_bytes(serialized_key);
181+
182+
assert_eq!(key, deserialized_key);
183+
}
121184
}

0 commit comments

Comments
 (0)