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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
// Copyright 2017, CZ.NIC z.s.p.o. (http://www.nic.cz/)
//
// This file is part of the pakon system.
//
// Pakon is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
// Pakon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Pakon.  If not, see <http://www.gnu.org/licenses/>.

//! Some basic functionality not directly related to the aggregator business.
//!
//! This module holds misc utilities that don't do the actual aggregation, but are needed anyway,
//! like logging setup or configuration loading.

#![deny(missing_docs)]

extern crate futures;
#[macro_use]
extern crate slog;
extern crate slog_async;
extern crate slog_scope;
extern crate slog_syslog;
extern crate slog_stdlog;
extern crate slog_term;
extern crate structopt;
#[macro_use]
extern crate structopt_derive;
extern crate syslog;

use std::ffi::OsString;
use std::io::{self, ErrorKind};
use std::str::FromStr;
use std::path::PathBuf;

use futures::Future;
use slog::{Discard, Drain, Duplicate, Never};
use slog_async::Async;
use slog_scope::GlobalLoggerGuard;
use slog_term::{CompactFormat, TermDecorator};
use structopt::{StructOpt, clap};
use syslog::Facility;

pub use slog::{Level, Logger};

pub mod tunable;

/// A newtype around `slog::Level`.
///
/// We need custom parsing routine that returns an error, so the argument parser works.
#[derive(Debug, PartialEq)]
struct LogLevel(Level);

// Delegate and decorate the FromStr parsing trait
impl FromStr for LogLevel {
    // Just *some* error that is easy to customize
    type Err = io::Error;
    fn from_str(level: &str) -> Result<Self, Self::Err> {
        Level::from_str(level)
            .map(LogLevel) // Wrap it up
            .map_err(|()| io::Error::new(ErrorKind::InvalidInput, "Invalid log level"))
    }
}

/// Create the root logger.
///
/// This constructs an appropriate root logger for the application.
///
/// We send the logs to two places. The syslog and stderr (properly colored and nicely formatted).
/// We don't expect failures, but ignore them at the terminating loggers (stderr or syslog), but
/// panic if there's an error sending to the logger thread.
///
/// The parameters describe on which level to log.
///
/// It also installs a compatibility layer for the log crate, for any libraries that may use it.
/// However, you need to keep the returned `GlobalLoggerGuard` alive for the lifetime of the
/// application for it to work (store it into a *named* variable, not into `_`).
pub fn logger(stderr_level: Level, syslog_level: Level) -> (Logger, GlobalLoggerGuard) {
    // The nice colored stderr logger
    let decorator = TermDecorator::new().stderr().build();
    let term = CompactFormat::new(decorator)
        .use_local_timestamp()
        .build()
        .filter_level(stderr_level);
    // Try to connect to syslog and provide a dummy drain if it is not possible
    let (sys, sys_err): (Box<Drain<Ok = (), Err = Never> + Send + 'static>, _) =
        match slog_syslog::unix_3164(Facility::LOG_DAEMON) {
            Ok(sys) => (Box::new(sys.filter_level(syslog_level).ignore_res()), None),
            Err(e) => (Box::new(Discard), Some(e)),
        };
    // Run it in a separate thread, both for performance and because the terminal one isn't Sync
    let async = Async::new(Duplicate::new(term, sys).ignore_res())
        // Especially in test builds, we have quite large bursts of messages, so have more space to
        // store them.
        .chan_size(2048)
        .build();
    let logger = Logger::root(async.ignore_res(),
                              o!("app" => format!("{}/{}",
                                                  env!("CARGO_PKG_NAME"),
                                                  env!("CARGO_PKG_VERSION"))));
    if let Some(e) = sys_err {
        error!(logger,
               "Failed to set up the syslog logger, logging to stderr only: {}",
               e);
    }
    // Some legacy libraries are very spammy. We filter them out and disable anything under debug
    // level and use it only in our code and configurable libraries.
    let legacy = logger.clone()
        .filter_level(Level::Info)
        .ignore_res();
    let guard = slog_scope::set_global_logger(Logger::root(legacy, o!("context" => "legacy")));
    // Can this actually fail? When?
    slog_stdlog::init().expect("Failed to initialize compatibility logging");
    (logger, guard)
}

/// A logger usable in tests.
///
/// This one simply does not log. It can be used whenever a logger is needed during tests (both
/// integration and unit tests).
pub fn test_logger() -> Logger {
    // Make sure there's some global scoped logger for this test. Also, don't remove it after the
    // test ends, because then other tests that run in parallel would miss it (the global logger is
    // shared).
    slog_scope::set_global_logger(Logger::root(slog::Discard, o!())).cancel_reset();
    Logger::root(slog::Discard, o!())
}

/// The parsed command line options.
///
/// This holds information that is provided on the command line. However, we want to convert it to
/// the real command line options.
#[derive(StructOpt, Debug, PartialEq)]
struct PreCmdlineOpts {
    /// Desired log level on stderr.
    #[structopt(long = "stderr-level", short = "E", help = "Log level to stderr",
                default_value = "Debug")]
    stderr_level: LogLevel,
    /// Desired log level for syslog.
    #[structopt(long = "syslog-level", short = "S", help = "Log level to syslog",
                default_value = "Info")]
    syslog_level: LogLevel,
    /// The pakon's report socket path.
    #[structopt(long = "guts", short = "g",
                help = "Path for the unix domain socket where the guts daemon listens and reports")]
    #[cfg(feature = "guts")]
    guts: Option<String>,
    /// The socket where our clients connect.
    #[structopt(long = "downstream", short = "d",
                help = "Path for the unix domain socket where our clients connect to us")]
    downstream: String,
}

/// The parsed command line options.
///
/// This holds information that is provided on the command line.
///
/// Note that we may decide to move some of these options into a configuration file.
#[derive(Debug, PartialEq)]
pub struct CmdlineOpts {
    /// Desired log level on stderr.
    pub stderr_level: Level,
    /// Desired log level for syslog.
    pub syslog_level: Level,
    /// The pakon's report socket path.
    #[cfg(feature = "guts")]
    pub guts: Option<PathBuf>,
    /// The pakon's listening socket.
    pub downstream: PathBuf,
}

impl From<PreCmdlineOpts> for CmdlineOpts {
    fn from(pre: PreCmdlineOpts) -> Self {
        CmdlineOpts {
            stderr_level: pre.stderr_level.0,
            syslog_level: pre.syslog_level.0,
            #[cfg(feature = "guts")]
            guts: pre.guts.map(Into::into),
            downstream: pre.downstream.into(),
        }
    }
}

/// Parse the command line arguments passed.
pub fn cmdline_opts<I, T>(iter: I) -> Result<CmdlineOpts, clap::Error>
    where T: Into<OsString> + Clone,
          I: IntoIterator<Item = T>
{
    let matches = PreCmdlineOpts::clap().get_matches_from_safe(iter)?;
    Ok(PreCmdlineOpts::from_clap(matches).into())
}

// Some convenient types reused through the code but not really belonging anywhere

/// A trait object for futures, so we can return them.
pub type LocalBoxFuture<T, E> = Box<Future<Item = T, Error = E>>;

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

    /// Some basic tests for command line parsing.
    ///
    /// Nothing extra, just check that garbage is refused, we get some default values, etc.
    #[test]
    #[cfg(feature = "guts")]
    fn cmdline_parse() {
        // Help is implemented as a special kind of error
        cmdline_opts(&["app", "--help"]).unwrap_err();
        // As is version
        cmdline_opts(&["app", "--version"]).unwrap_err();
        // If we are using an unknown option, it is refused
        cmdline_opts(&["app", "--garbage"]).unwrap_err();
        // No options means defaults where we have some
        assert_eq!(cmdline_opts(&["app", "--guts", "g", "--downstream", "d"]).unwrap(),
                   CmdlineOpts {
                       stderr_level: Level::Debug,
                       syslog_level: Level::Info,
                       guts: Some("g".into()),
                       downstream: "d".into(),
                   });
        // But no options whatsoever is refused, as we're missing some mandatory config
        cmdline_opts(&["app"]).unwrap_err();
        // We can override the log levels
        assert_eq!(cmdline_opts(&["app",
                                  "--syslog-level",
                                  "trace",
                                  "--stderr-level",
                                  "trace",
                                  "--guts",
                                  "g",
                                  "--downstream",
                                  "d"])
                           .unwrap(),
                   CmdlineOpts {
                       stderr_level: Level::Trace,
                       syslog_level: Level::Trace,
                       guts: Some("g".into()),
                       downstream: "d".into(),
                   });

        // Wrong option value is refused
        cmdline_opts(&["app", "--syslog-level", "no-such-level"]).unwrap_err();
    }

    /// Logging setup tests.
    ///
    /// As we don't examine the output in syslog or stderr (too much work for little gain), we just
    /// run the setups and check it doesn't crash, nothing more.
    #[test]
    fn log_init() {
        let (_logger, guard) = logger(Level::Critical, Level::Critical);
        // As the tests run in parallel, the global scoped log is shared, we can't remove it once
        // this test ends.
        guard.cancel_reset();
    }
}