1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under both the MIT license found in the
 * LICENSE-MIT file in the root directory of this source tree and the Apache
 * License, Version 2.0 found in the LICENSE-APACHE file in the root directory
 * of this source tree.
 */

//! Provides ways to control how the KV values passed to slog macros are printed

use std::str::FromStr;

use failure_ext::SlogKVErrorKey;
use slog::{Key, Level};

/// The KV value is being processed based on the category it is bucketed in
#[derive(Debug, PartialEq, Eq)]
pub enum KVCategory {
    /// KV value is not printed at all
    Ignore,
    /// KV value is inlined with the main message passed to slog macro
    Inline,
    /// KV value is printed as a separate line with the provided log level
    LevelLog(Level),
}

/// Structures implementing this trait are being used to categorize the KV values into one of the
/// `KVCategory`.
pub trait KVCategorizer {
    /// For a given key from KV decide which category it belongs to
    fn categorize(&self, key: Key) -> KVCategory;
    /// For a given key from KV return a name that should be printed for it
    fn name(&self, key: Key) -> &'static str;
    /// True if category of a given key is KVCategory::Ignore
    fn ignore(&self, key: Key) -> bool {
        self.categorize(key) == KVCategory::Ignore
    }
}

/// Dummy categorizer that inlines all KV values with names equal to key
pub struct InlineCategorizer;
impl KVCategorizer for InlineCategorizer {
    fn categorize(&self, _key: Key) -> KVCategory {
        KVCategory::Inline
    }

    fn name(&self, key: Key) -> &'static str {
        key
    }
}

/// Used to properly print `error_chain` `Error`s. It displays the error and it's causes in
/// separate log lines as well as backtrace if provided.
/// The `error_chain` `Error` must implement `KV` trait. It is recommended to use `impl_kv_error`
/// macro to generate the implementation.
pub struct ErrorCategorizer;
impl KVCategorizer for ErrorCategorizer {
    fn categorize(&self, key: Key) -> KVCategory {
        match SlogKVErrorKey::from_str(key) {
            Ok(SlogKVErrorKey::Error) => KVCategory::LevelLog(Level::Error),
            Ok(SlogKVErrorKey::Cause) => KVCategory::LevelLog(Level::Debug),
            Ok(SlogKVErrorKey::Backtrace) => KVCategory::LevelLog(Level::Trace),
            Ok(SlogKVErrorKey::RootCause) | Err(()) => InlineCategorizer.categorize(key),
            Ok(SlogKVErrorKey::ErrorDebug) => KVCategory::LevelLog(Level::Debug),
        }
    }

    fn name(&self, key: Key) -> &'static str {
        match SlogKVErrorKey::from_str(key) {
            Ok(SlogKVErrorKey::Error) => "Error",
            Ok(SlogKVErrorKey::Cause) => "Caused by",
            Ok(SlogKVErrorKey::Backtrace) => "Originated in",
            Ok(SlogKVErrorKey::RootCause) => "Root cause",
            Ok(SlogKVErrorKey::ErrorDebug) => "Debug context",
            Err(()) => InlineCategorizer.name(key),
        }
    }
}

/// Categorizer to be used by all Facebook services
pub struct FacebookCategorizer;
impl KVCategorizer for FacebookCategorizer {
    fn categorize(&self, key: Key) -> KVCategory {
        match key {
            "hostname" => KVCategory::Ignore,
            _ => ErrorCategorizer.categorize(key),
        }
    }

    fn name(&self, key: Key) -> &'static str {
        match key {
            _ => ErrorCategorizer.name(key),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use anyhow::Error;
    use failure_ext::SlogKVError;
    use itertools::assert_equal;
    use slog::{b, record, KV};
    use thiserror::Error;

    use crate::collector_serializer::CollectorSerializer;

    #[derive(Error, Debug)]
    enum TestError {
        #[error("my error #{0} displayed")]
        MyError(usize),
    }

    #[test]
    fn test_inline() {
        let categorizer = InlineCategorizer;
        let values = vec!["test", "test2"];
        for v in values {
            assert_eq!(categorizer.categorize(v), KVCategory::Inline);
            assert_eq!(categorizer.name(v), v);
        }
    }

    #[test]
    fn test_error() {
        let err = Error::from(TestError::MyError(0))
            .context(TestError::MyError(1))
            .context(TestError::MyError(2));
        let debug = format!("{:#?}", err);

        let categorizer = ErrorCategorizer;
        let mut serializer = CollectorSerializer::new(&categorizer);
        SlogKVError(err)
            .serialize(
                &record!(Level::Info, "test", &format_args!(""), b!()),
                &mut serializer,
            )
            .expect("failed to serialize");
        assert_equal(
            serializer.into_inner(),
            vec![
                ("error", "my error #2 displayed".to_owned()),
                ("error_debug", debug),
                ("cause", "my error #1 displayed".to_owned()),
                ("cause", "my error #0 displayed".to_owned()),
                ("root_cause", "my error #0 displayed".to_owned()),
            ],
        );
    }
}