Logging

Предадени решения

Краен срок:
18.12.2018 17:00
Точки:
20

Срокът за предаване на решения е отминал

use std::cell::RefCell;
use std::io::{self, Write};
use std::rc::Rc;
use std::time::{Instant, Duration};
extern crate solution;
use solution::*;
#[derive(Clone)]
struct TestWriter {
storage: Rc<RefCell<Vec<u8>>>,
}
impl TestWriter {
fn new() -> Self {
TestWriter { storage: Rc::new(RefCell::new(Vec::new())) }
}
fn into_inner(self) -> Vec<u8> {
Rc::try_unwrap(self.storage).ok().unwrap().into_inner()
}
fn into_string(self) -> String {
String::from_utf8(self.into_inner()).unwrap()
}
}
impl Write for TestWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.storage.borrow_mut().write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.storage.borrow_mut().flush()
}
}
struct ErroringMockIO {}
impl Write for ErroringMockIO {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
Err(io::Error::new(io::ErrorKind::Other, "Write Error!"))
}
fn flush(&mut self) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::Other, "Flush Error!"))
}
}
#[test]
fn test_basic_push() {
let mut logger = BufferedLogger::new(TestWriter::new(), 100);
let now = Instant::now();
logger.push(now + Duration::from_millis(1), "Warning!");
logger.push(now + Duration::from_millis(2), "!");
logger.push(now + Duration::from_millis(3), "info");
logger.push(now + Duration::from_millis(4), "asdf");
assert_eq!(
logger.buffered_entries().as_ref(),
["Warning!", "!", "info", "asdf"],
)
}
#[test]
fn test_basic_log() {
let mut logger = BufferedLogger::new(TestWriter::new(), 100);
logger.log("Some warning");
assert_eq!(
logger.buffered_entries().as_ref(),
[format!("Some warning")]
)
}
#[test]
fn test_flushing_the_buffer() {
let out = TestWriter::new();
{
let mut logger = BufferedLogger::new(out.clone(), 100);
let now = Instant::now();
logger.push(now + Duration::from_millis(1), "Some warning");
logger.push(now + Duration::from_millis(2), "Some other warning");
logger.flush();
assert_eq!(logger.buffered_entries().len(), 0);
}
assert_eq!(out.into_string(), vec![
"Some warning\n",
"Some other warning\n",
].join(""));
}
#[test]
fn test_reordering_logs_in_buffer() {
let mut logger = BufferedLogger::new(TestWriter::new(), 100);
let now = Instant::now();
logger.push(now + Duration::from_millis(4), "Fourth");
logger.push(now + Duration::from_millis(2), "Second");
logger.push(now + Duration::from_millis(3), "Third");
logger.push(now + Duration::from_millis(1), "First");
assert_eq!(logger.buffered_entries(), vec![
"First",
"Second",
"Third",
"Fourth",
]);
}
#[test]
fn test_reordering_logs_in_io() {
let out = TestWriter::new();
{
let mut logger = BufferedLogger::new(out.clone(), 100);
let now = Instant::now();
logger.push(now + Duration::from_millis(4), "Fourth");
logger.push(now + Duration::from_millis(2), "Second");
logger.push(now + Duration::from_millis(3), "Third");
logger.push(now + Duration::from_millis(1), "First");
logger.flush();
}
assert_eq!(out.into_string(), vec![
"First\n",
"Second\n",
"Third\n",
"Fourth\n",
].join(""));
}
#[test]
fn test_cloning_a_logger_shares_a_buffer() {
let out = TestWriter::new();
let mut first_logger = BufferedLogger::new(out, 100);
let mut second_logger = first_logger.clone();
let mut third_logger = second_logger.clone();
let now = Instant::now();
first_logger.push(now + Duration::from_millis(2), "Second");
third_logger.push(now + Duration::from_millis(1), "First");
second_logger.push(now + Duration::from_millis(3), "Third");
assert_eq!(first_logger.buffered_entries(), vec![
"First", "Second", "Third"
]);
}
#[test]
fn test_cloning_a_logger_shares_their_io() {
let out = TestWriter::new();
{
let mut first_logger = BufferedLogger::new(out.clone(), 100);
let mut second_logger = first_logger.clone();
let mut third_logger = second_logger.clone();
let now = Instant::now();
first_logger.push(now + Duration::from_millis(2), "Second");
third_logger.push(now + Duration::from_millis(1), "First");
second_logger.push(now + Duration::from_millis(3), "Third");
first_logger.flush()
}
assert_eq!(out.into_string(), vec![
"First\n",
"Second\n",
"Third\n",
].join(""));
}
#[test]
fn test_automatic_flushing_when_buffer_limit_is_reached() {
let out = TestWriter::new();
let now = Instant::now();
{
let mut logger = BufferedLogger::new(out.clone(), 3);
logger.push(now + Duration::from_millis(1), "One");
logger.push(now + Duration::from_millis(2), "Two");
assert_eq!(logger.buffered_entries().len(), 2);
logger.push(now + Duration::from_millis(3), "Three");
assert_eq!(logger.buffered_entries().len(), 0);
logger.push(now + Duration::from_millis(4), "One");
assert_eq!(logger.buffered_entries().len(), 1);
}
assert_eq!(out.into_string(), "One\nTwo\nThree\n");
}
#[test]
fn test_automatic_flushing_when_zero_buffer_limit() {
let out = TestWriter::new();
let now = Instant::now();
{
let mut logger = BufferedLogger::new(out.clone(), 0);
logger.push(now + Duration::from_millis(1), "One");
assert_eq!(logger.buffered_entries().len(), 0);
logger.push(now + Duration::from_millis(2), "Two");
assert_eq!(logger.buffered_entries().len(), 0);
logger.push(now + Duration::from_millis(3), "Three");
assert_eq!(logger.buffered_entries().len(), 0);
}
assert_eq!(out.into_string(), "One\nTwo\nThree\n");
}
#[test]
fn test_multilogger_logs_to_several_ios() {
let out1 = TestWriter::new();
let out2 = TestWriter::new();
let now = Instant::now();
{
let logger1 = BufferedLogger::new(out1.clone(), 100);
let logger2 = BufferedLogger::new(out2.clone(), 100);
let mut multi = MultiLogger::new();
multi.log_to(logger1);
multi.log_to(logger2);
multi.push(now + Duration::from_millis(1), "One");
multi.push(now + Duration::from_millis(2), "Two");
multi.flush();
}
assert_eq!(out1.into_string(), "One\nTwo\n");
assert_eq!(out2.into_string(), "One\nTwo\n");
}
#[test]
fn test_logger_combinations() {
let out = TestWriter::new();
{
let base1 = BufferedLogger::new(out.clone(), 100);
let scoped1 = ScopedLogger::new("Base1", base1.clone());
let base2 = BufferedLogger::new(out.clone(), 100);
let scoped2 = ScopedLogger::new("Base2", base2.clone());
let mut multi = MultiLogger::new();
multi.log_to(scoped1);
multi.log_to(scoped2);
let mut outer = ScopedLogger::new("Multi", multi);
outer.log("Test entry");
assert_eq!(base1.buffered_entries(), vec!["[Base1] [Multi] Test entry"]);
assert_eq!(base2.buffered_entries(), vec!["[Base2] [Multi] Test entry"]);
outer.flush();
}
assert_eq!(out.into_string(), "[Base1] [Multi] Test entry\n[Base2] [Multi] Test entry\n");
}
#[test]
fn test_multilogger_logs_and_flushes_when_needed() {
let out1 = TestWriter::new();
let out2 = TestWriter::new();
let now = Instant::now();
{
let logger1 = BufferedLogger::new(out1.clone(), 3);
let logger2 = BufferedLogger::new(out2.clone(), 3);
let mut multi = MultiLogger::new();
multi.log_to(logger1.clone());
multi.push(now + Duration::from_millis(1), "One");
multi.log_to(logger2.clone());
multi.push(now + Duration::from_millis(2), "Two");
multi.push(now + Duration::from_millis(3), "Three");
assert_eq!(logger1.buffered_entries().len(), 0);
assert_eq!(logger2.buffered_entries().len(), 2);
}
}
#[test]
fn test_scoped_logger() {
let out = TestWriter::new();
let now = Instant::now();
{
let base = BufferedLogger::new(out.clone(), 100);
let mut first_logger = ScopedLogger::new("First", base.clone());
let mut second_logger = ScopedLogger::new("Second", base.clone());
first_logger.push(now + Duration::from_millis(1), "One");
second_logger.push(now + Duration::from_millis(2), "Two");
assert_eq!(base.buffered_entries(), vec!["[First] One", "[Second] Two"]);
second_logger.push(now + Duration::from_millis(3), "Three");
first_logger.push(now + Duration::from_millis(4), "Four");
first_logger.flush();
second_logger.flush();
}
assert_eq!(out.into_string(), "[First] One\n[Second] Two\n[Second] Three\n[First] Four\n");
}
#[test]
fn test_scoped_logger_with_a_string_tag() {
let out = TestWriter::new();
{
let base = BufferedLogger::new(out.clone(), 100);
let mut logger = ScopedLogger::new(&String::from("First"), base.clone());
logger.log("Test");
logger.try_flush().unwrap();
}
assert_eq!(out.into_string(), "[First] Test\n");
}
#[test]
fn test_erroring_io() {
let out = ErroringMockIO {};
let mut logger = BufferedLogger::new(out, 2);
logger.log("One");
if let Ok(_) = logger.try_flush() {
assert!(false, "Expected try_flush with an erroring IO to return an error")
}
logger.flush(); // Should work, no errors
logger.log("Two");
logger.log("Something");
// Should flush successfully, no errors.
}

В тази задача, ще имплементирате структури, които да се използват за logging (писане на информация за протичането на програмата, в някой файл, или на стандартния изход). Трите структури са:

  • BufferedLogger, който събира съобщения, и, когато броя им надмине определена граница, ги записва в подадения "файл" (тип, който имплементира Write), подредени по timestamp.
  • MultiLogger, който ще ви позволи да log-вате в няколко "файла" наведнъж -- примерно, в истински файл, в syslog, и на стандартния изход.
  • ScopedLogger, който ще prefix-не всеки logger с определен низ, позволяващ ви различни структури да си държат reference към споделен logger с различен tag, който да ги идентифицира.

За да може да се композират, първо ще си дефинирате споделен trait.

Logger trait

Очакваме дефиниция, подобна на това:

use std::time::Instant;
use std::io;

pub trait Logger {
    /// Метод, който добавя нов запис за логване. Първия аргумент, от тип `std::time::Instant`, е
    /// момента във времето, който се асоциира със събитието. Обикновено ще се ползва метода `log`
    /// директно, който запълва този параметър, но метода `push` ще е удобен за тестване на
    /// логиката.
    ///
    /// Втория аргумент е низа, който ще се логва.
    ///
    fn push(&mut self, time: Instant, text: &str);

    /// Метод който ще работи като `push`, с тази разлика, че директно използва `Instant::now()` за
    /// да вземе текущ timestamp.
    ///
    fn log(&mut self, text: &str);

    /// Метод, който записва нещата от вътрешния буфер към някакъв външен носител -- файл, сокет,
    /// стандартния изход. В случай на имплементация, която няма нужда от този метод, винаги може да
    /// се имплементира като просто `Ok(())`.
    ///
    fn try_flush(&mut self) -> io::Result<()>;

    /// Метод, който прави същото като по-горния, но не връща грешка. Вижте по-долу за бележки за
    /// Error handling-а, който очакваме.
    ///
    fn flush(&mut self);
}

Имайте предвид, че някои от тези методи вероятно могат да получат default-ни имплементации директно в trait-а. Стига това да се компилира с базовия тест, това би било ок.

Error handling

Какво може да направим, ако един Logger се опита да пише във файл и това не проработи? Може файла да изчезне, защото е на network drive и интернета пада, може да бъде изтрит, може харддиска да се счупи.

Бихме могли да изискваме от всяка IO операция да връща грешка, но това ще направи употребата на logger-а ужасно неудобна. Бихме могли да викаме unwrap, но ако нещо се случи с log-ването, това не значи, че останалата част от кода не работи -- вероятно не е добра идея да спираме цялата програма, понеже log-ването се е счупило.

Следния подход ще свърши работа за целите ни: Метода try_flush ще връща всякакви IO грешки, които се случат, и ще тестваме за това. Метода flush няма да връща никаква грешка, но няма да спира и програмата -- вместо това, ще напечата грешката на стандартния изход за грешки, използвайки (например) макроса eprintln!. Където метод не връща io грешка, очакваме да се справяте с грешките по този начин.

BufferedLogger

use std::io::Write;

pub struct BufferedLogger<W: Write> {
    /// Каквито полета ви трябват
}

impl<W: Write> BufferedLogger<W> {
    /// Конструира структура, която ще пази записи в буфер с размер `buffer_size`, и ще ги записва
    /// в подадената структура от тип, който имплементира `Write`;
    ///
    pub fn new(out: W, buffer_size: usize) -> Self {
        unimplemented!()
    }

    /// Връща списък от записите, които са буферирани в момента. Записите се очаква да бъдат
    /// подредени по времето, в което са log-нати, от най-ранни до най-късни.
    ///
    pub fn buffered_entries(&self) -> Vec<String> {
        unimplemented!()
    }
}

/// Вижте по-долу за бележки за клонирането
impl<W: Write> Clone for BufferedLogger<W> {
    fn clone(&self) -> Self {
        unimplemented!()
    }
}

impl<W: Write> Logger for BufferedLogger<W> {
    /// Подходящи имплементации на Logger методите
}

Това е най-базовия logger който ще напишете, и другите два ще използват него като основа. Всяко викане на push или log ще добавя нов запис в запазения буфер.

let mut buffered_logger = BufferedLogger::new(io::stdout(), 100);
let now = Instant::now();

buffered_logger.push(now + Duration::from_millis(2), "Test2");
buffered_logger.push(now + Duration::from_millis(1), "Test1");

assert_eq!(buffered_logger.buffered_entries(), vec!["Test1", "Test2"]);

Забележете, че записите са подредени в ред на подаденото време.

Flush-ване

Когато извикаме метода try_flush или метода flush, очакваме всички записи в буфера да бъдат записани в подадения тип, който имплементира Write (подредени по време), и да бъдат премахнати от буфера. Тоест:

logger.flush();
assert_eq!(logger.buffered_entries().len(), 0);

В случай, че при викане на push или log броя записи в буфера надхвърли подадения buffer_size, очакваме да се викне flush след като се сложи новия запис в буфера:

let mut logger = BufferedLogger::new(io::stdout(), 3);
logger.log("Test");
logger.log("Test");
assert_eq!(logger.buffered_entries().len(), 2);
logger.log("Test");
assert_eq!(logger.buffered_entries().len(), 0);

Писане във "файл"

Важно е да се погрижете, че всеки ред ще завършва със символ за нов ред, \n. Ако log-нем низовете Foo и Bar, и flush-нем, очакваме съдържанието на "файла" да е Foo\nBar\n. Ще тестваме тези неща, така че ако допуснете грешка тук, ще губите точки. Силно ви съветваме да си помислите как да изтествате четенето и писането във "файл" (иначе казано, в "нещо, което имплементира Write"), за да сте сигурни, че сте го имплементирали правилно.

Клониране

BufferedLogger може да бъде клониран, и очакваме клонингите да споделят общ буфер с оригинала. Тоест, ако имаме два logger-а, които са клонирани, и викнем log на единия, и после buffered_entries на другия, трябва да "видим" същата стойност в буфера.

За целта може да използвате Rc, за да споделят два клонинга общ буфер и общ "файл", и RefCell, за да можете да извиквате методи, които имат нужда от mutable reference към това, което споделяте. (Има и други начини, бъдете свободни да ги имплементирате, ако ги намерите.) Разгледайте внимателно документацията на двата типа и/или си припомнете лекциите за свързани списъци.

MultiLogger

Този logger ще бъде инстанциран и след това ще може да приеме няколко logger-а, към които ще делегира виканията на методите си:

let logger1 = BufferedLogger::new(io::stdout(), 100);
let logger2 = BufferedLogger::new(io::stdout(), 100);
let mut logger = MultiLogger::new();
logger.log_to(logger1.clone());
logger.log_to(logger2.clone());

При викане на метод от Logger интерфейса, ще минете през "вложените" logger-и, каквито и те да са, и ще им подавате нужните методи.

pub struct MultiLogger {
    /// Каквито полета решите, че ви трябват
}

impl MultiLogger {
    pub fn new() -> Self {
        unimplemented!()
    }

    pub fn log_to<L: Logger + 'static>(&mut self, logger: L) {
        unimplemented!()
    }
}

impl Logger for MultiLogger {
    /// Подходящи имплементации на Logger методите
}

Нещо важно, което трябва да имате предвид -- logger-ите могат да бъдат от различни конкретни типове:

let logger1 = BufferedLogger::new(io::stdout(), 100);
let logger2 = BufferedLogger::new(io::stdout(), 100);

let mut logger = MultiLogger::new();
logger.log_to(logger1.clone());
logger.log_to(ScopedLogger::new("Second", logger2.clone()));

За да постигнете това, съветваме ви да съхранявате всеки конкретен logger в trait object -- Box<dyn Logger>.

ScopedLogger

Последния logger ще приеме един вложен logger и низ, който да се използва като "етикет". Ще работи горе-долу така:

let base = BufferedLogger::new(io::stdout(), 100);
let logger = ScopedLogger::new("FMI", base);
logger.log("Test");

assert_eq!(base.buffered_entries(), vec!["[FMI] Test"]);

Очаквана дефиниция:

pub struct ScopedLogger<L: Logger> {
    /// Каквито полета решите, че ви трябват
}

impl<L: Logger> ScopedLogger<L> {
    pub fn new(tag: &str, base_logger: L) -> Self {
        unimplemented!()
    }
}

impl<L: Logger> Logger for ScopedLogger<L> {
    /// Подходящи имплементации на Logger методите
}

Забележете, че "tag"-а, който подаваме, се обвива в квадратни скоби, и има един интервал преди истинския текст. Нищо не ви пречи и да вложите няколко такива. Всеки нов "обвиващ" logger добавя своя tag в началото на подадения низ. Това може да доведе до малко странно изглеждащи логове, но би трябвало да е сравнително лесно за имплементация:

let base = BufferedLogger::new(io::stdout(), 100);
let mut logger = ScopedLogger::new("Rust", ScopedLogger::new("FMI", base.clone()));
logger.log("Test");

assert_eq!(base.buffered_entries(), vec!["[FMI] [Rust] Test"]);

Задължително прочетете (или си припомнете): Указания за предаване на домашни