daboross / fern

Simple, efficient logging for Rust
MIT License
848 stars 51 forks source link

JSON #62

Closed Raniz85 closed 4 years ago

Raniz85 commented 4 years ago

I'm trying to implement JSON logging using fern, but I'm having issues with messages containing newlines.

See this example program:

fn main() {
    let mut dispatch = fern::Dispatch::new()
        .format(|out, message, record| {
            out.finish(format_args!(
                "{{ \"message\": \"{}\" }}",
                message
            ))
        })
        .chain(std::io::stdout())
        .apply().unwrap();
    log::info!("This is a single line and works fine");
    log::info!("This is mulitple lines\n and doesn't produce valid JSON");
}

And the output:

{ "message": "This is a single line and works fine" }
{ "message": "This is mulitple lines
 and doesn't produce valid JSON" }

Is there any good way of escaping the newline(s) in the message?

daboross commented 4 years ago

If you're OK with it, how about using an existing json crate? It'll have robust handling of this kind of issue.

For instance, using serde_json, I think something like the following should work:

fn main() {
    let mut dispatch = fern::Dispatch::new()
        .format(|out, message, record| {
            out.finish(format_args!(
                "{}",
                serde_json::to_string(&serde_json::json!(
                    {
                        "message": message,
                    }
                )).expect("formatting `serde_json::Value` with string keys never fails")
            ))
        })
        .chain(std::io::stdout())
        .apply().unwrap();
    log::info!("This is a single line and works fine");
    log::info!("This is mulitple lines\n and doesn't produce valid JSON");
}

This has the disadvantage of making multiple allocations per message, but it'll be quite robust.

If serde_json is too heavy weight, then tinyjson might be better? It has a much lower compile-time cost, and the runtime will probably be slightly faster as well.

fn main() {
    let mut dispatch = fern::Dispatch::new()
        .format(|out, message, record| {
            out.finish(format_args!(
                "{{\"message\":{}}}",
                tinyjson::JsonValue::String(message.to_string()).stringify()
                .expect("no inf floats in a string")
            ))
        })
        .chain(std::io::stdout())
        .apply().unwrap();
    log::info!("This is a single line and works fine");
    log::info!("This is mulitple lines\n and doesn't produce valid JSON");
}

Let me know what your constraints are, and we might be able to find a better solution?

Raniz85 commented 4 years ago

Using serde_json hadn't occured to me, but that works really well.

Thanks!

Maybe mention this in the docs? :)

daboross commented 4 years ago

I'd originally thought that structured logging would be coming to log a lot sooner and we'd be able to add builtin json/other serde logging based on that. But I think you might be right - adding documentation for doing this with regular logging would be a good idea.

Raniz85 commented 4 years ago

Key/Value logging seems to have arrived in the log crate now.

https://docs.rs/log/0.4.8/log/struct.Record.html#method.key_values

daboross commented 4 years ago

Oh! I must have not been watching the right issues/etc. I'll have to go back and look at when that happened, and see if there's anything we want to change in fern based on that.

Thanks!

daboross commented 4 years ago

Looks like it's still unstable right now, but other logging crates have already started integrating into it.

I've opened https://github.com/daboross/fern/issues/63 to do some initial brainstorming for how fern should support this. I probably won't have time to actually do the planning nor implementation soon, but this will track it for the future.