daboross/fern

Color codes in output files

sigod opened this issue · 7 comments

sigod commented
[2019-09-29 22:37:40][�[32mINFO �[0m][test] Hello, world!
[2019-09-29 22:37:40][�[33mWARN �[0m][test] Hello, world!
[2019-09-29 22:37:40][�[31mERROR�[0m][test] Hello, world!

Is there a way to prevent fern from writing color codes into files?

You can have two Dispatches, one for terminal and one for the file. For the terminal one you set colors and for the file you don't.

As an example solution, you might do something like the following:

let colors_line = ColoredLevelConfig::new()
    .error(Color::Red)
    .warn(Color::Yellow)
    .trace(Color::BrightBlack);

let colors_level = colors_line.clone().info(Color::Green);
fern::Dispatch::new()
    .level(log::LevelFilter::Warn)
    .chain(
        fern::Dispatch::new()
            .format(move |out, message, record| {
                out.finish(format_args!(
                    "{color_line}[{date}][{target}][{level}{color_line}] {message}\x1B[0m",
                    color_line = format_args!(
                        "\x1B[{}m",
                        colors_line.get_color(&record.level()).to_fg_str()
                    ),
                    date = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
                    target = record.target(),
                    level = colors_level.color(record.level()),
                    message = message,
                ));
            })
            .chain(std::io::stdout()),
    )
    .chain(
        fern::Dispatch::new()
            .format(|out, message, record| {
                out.finish(format_args!(
                    "[{date}][{target}][{level}] {message}\x1B[0m",
                    date = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
                    target = record.target(),
                    level = colors_level.color(record.level()),
                    message = message,
                ));
            })
            .chain(/* non-colored output */),
    )
    .apply()
    .unwrap();

This is currently very verbose, and so a better solution would indeed be a good idea. But, for the moment, this should at least function well.

The correct way to handle this is to detect the output type, eg. tty or file, and disable colors if not a tty. I ran into this recently, and happy to look into it.

I think fixing this through detection of output would require significantly rearchitecting fern's logging pipeline. There might be a workaround we could implement, but it really isn't designed to have information flow from the final output back to the formatters.

That isn't to say that re-architecting it wouldn't be worth it - just that I don't expect it to be trivial, in case you do want to take it on.

A more compromising solution I've been considering would be to add a "remove color" logging wrapper which could be used when printing to files. At least, I think we'd be able to implement this without having to rework fern's internals.

If you're interested in doing this with either strategy, though, be my guest! I'm happy to provide any guidance & review PRs.

Hmmm, yeah that does sound complicated. Alternatively, if it was easier to parametrize the color config, or if there was a color config such that no colors were used, that would make it easier.

This is how I currently have it:

        let colors = ColoredLevelConfig::new().info(Color::Green);
        let stream = Stream::Stderr;
        let io = std::io::stderr();
        let isatty = atty::is(stream);

        fern::Dispatch::new()
            .format(move |out, message, record| {
                if isatty {
                    out.finish(format_args!(
                        "{:5} [{}] {}",
                        colors.color(record.level()),
                        record.target(),
                        message
                    ))
                } else {
                    out.finish(format_args!(
                        "{:5} [{}] {}",
                        record.level(),
                        record.target(),
                        message
                    ))
                }
            })
            .level(opts.log)
            .chain(io)
            .apply()
            .unwrap();
    }

It would be much better if I could do, for eg.:

        let stream = Stream::Stderr;
        let io = std::io::stderr();
        let colors = if atty::is(stream) {
            ColoredLevelConfig::new().info(Color::Green)
        } else {
            ColoredLevelConfig::no_colors() // this is a config that doesn't use colors
        };

        fern::Dispatch::new()
            .format(move |out, message, record| {
                out.finish(format_args!(
                    "{:5} [{}] {}",
                    colors.color(record.level()),
                    record.target(),
                    message
                ))
            })
            .level(opts.log)
            .chain(io)
            .apply()
            .unwrap();
    }

The problem is ColoredLevelConfig always outputs color codes, and so does WithFgColor, and the types of record.level() and colors.color(record.level()) differ, which makes it pretty cumbersome..

@cloudhead You can do record.level().to_string() and colors.color(record.level()).to_string() to ensure they have the same type.

This is how I solved this issue:

pub fn setup_logger() -> Result<(), fern::InitError> {
    
    let mut colors = ColoredLevelConfig::new().info(Color::Green);

    let make_formatter = |use_colors: bool| {
        move |out: FormatCallback, message: &Arguments, record: &Record| {
            out.finish(format_args!(
                "{} {} [{}:{}] {}",
                chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
                if use_colors {
                    colors.color(record.level()).to_string()
                } else {
                    record.level().to_string()
                },
                record.file().unwrap_or("?".into()),
                record
                    .line()
                    .map(|l| l.to_string())
                    .unwrap_or(String::new()),
                message
            ))
        }
    };

    let default_log_level = log::LevelFilter::Trace;

    let file_dispatcher = fern::Dispatch::new()
        .format(make_formatter(false))
        .level(default_log_level)
        .chain(fern::log_file("output.log")?);

    let stdout_dispatcher = fern::Dispatch::new()
        .format(make_formatter(true))
        .level(default_log_level)
        .chain(std::io::stdout());

    fern::Dispatch::new()
        .chain(stdout_dispatcher)
        .chain(file_dispatcher)
        .apply()?;

    Ok(())
}

Btw I found https://docs.rs/strip-ansi-escapes/0.1.0/strip_ansi_escapes/, might be interesting for the "color stripper" approach.