Logging

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

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

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

В тази задача, ще имплементирате структури, които да се използват за 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"]);